Merge branch 'oauth2_provider' into 'master'
Oauth2 provider Please update #1713 See merge request !1346
This commit is contained in:
commit
ed932d82db
41 changed files with 983 additions and 35 deletions
2
Gemfile
2
Gemfile
|
@ -29,6 +29,8 @@ gem 'omniauth-twitter'
|
||||||
gem 'omniauth-github'
|
gem 'omniauth-github'
|
||||||
gem 'omniauth-shibboleth'
|
gem 'omniauth-shibboleth'
|
||||||
gem 'omniauth-kerberos'
|
gem 'omniauth-kerberos'
|
||||||
|
gem 'doorkeeper', '2.0.1'
|
||||||
|
gem "rack-oauth2", "~> 1.0.5"
|
||||||
|
|
||||||
# Extracting information from a git repository
|
# Extracting information from a git repository
|
||||||
# Provide access to Gitlab::Git library
|
# Provide access to Gitlab::Git library
|
||||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -37,6 +37,7 @@ GEM
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
arel (5.0.1.20140414130214)
|
arel (5.0.1.20140414130214)
|
||||||
asciidoctor (0.1.4)
|
asciidoctor (0.1.4)
|
||||||
|
attr_required (1.0.0)
|
||||||
awesome_print (1.2.0)
|
awesome_print (1.2.0)
|
||||||
axiom-types (0.0.5)
|
axiom-types (0.0.5)
|
||||||
descendants_tracker (~> 0.0.1)
|
descendants_tracker (~> 0.0.1)
|
||||||
|
@ -107,6 +108,8 @@ GEM
|
||||||
diff-lcs (1.2.5)
|
diff-lcs (1.2.5)
|
||||||
diffy (3.0.3)
|
diffy (3.0.3)
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
|
doorkeeper (2.0.1)
|
||||||
|
railties (>= 3.1)
|
||||||
dotenv (0.9.0)
|
dotenv (0.9.0)
|
||||||
dropzonejs-rails (0.4.14)
|
dropzonejs-rails (0.4.14)
|
||||||
rails (> 3.1)
|
rails (> 3.1)
|
||||||
|
@ -250,6 +253,7 @@ GEM
|
||||||
json (~> 1.8)
|
json (~> 1.8)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
httpauth (0.2.1)
|
httpauth (0.2.1)
|
||||||
|
httpclient (2.5.3.3)
|
||||||
i18n (0.6.11)
|
i18n (0.6.11)
|
||||||
ice_nine (0.10.0)
|
ice_nine (0.10.0)
|
||||||
jasmine (2.0.2)
|
jasmine (2.0.2)
|
||||||
|
@ -368,6 +372,12 @@ GEM
|
||||||
rack (>= 1.1.3)
|
rack (>= 1.1.3)
|
||||||
rack-mount (0.8.3)
|
rack-mount (0.8.3)
|
||||||
rack (>= 1.0.0)
|
rack (>= 1.0.0)
|
||||||
|
rack-oauth2 (1.0.8)
|
||||||
|
activesupport (>= 2.3)
|
||||||
|
attr_required (>= 0.0.5)
|
||||||
|
httpclient (>= 2.2.0.2)
|
||||||
|
multi_json (>= 1.3.6)
|
||||||
|
rack (>= 1.1)
|
||||||
rack-protection (1.5.1)
|
rack-protection (1.5.1)
|
||||||
rack
|
rack
|
||||||
rack-test (0.6.2)
|
rack-test (0.6.2)
|
||||||
|
@ -616,6 +626,7 @@ DEPENDENCIES
|
||||||
devise (= 3.2.4)
|
devise (= 3.2.4)
|
||||||
devise-async (= 0.9.0)
|
devise-async (= 0.9.0)
|
||||||
diffy (~> 3.0.3)
|
diffy (~> 3.0.3)
|
||||||
|
doorkeeper (= 2.0.1)
|
||||||
dropzonejs-rails
|
dropzonejs-rails
|
||||||
email_spec
|
email_spec
|
||||||
enumerize
|
enumerize
|
||||||
|
@ -672,6 +683,7 @@ DEPENDENCIES
|
||||||
rack-attack
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-mini-profiler
|
rack-mini-profiler
|
||||||
|
rack-oauth2 (~> 1.0.5)
|
||||||
rails (~> 4.1.0)
|
rails (~> 4.1.0)
|
||||||
rails_autolink (~> 1.1)
|
rails_autolink (~> 1.1)
|
||||||
rails_best_practices
|
rails_best_practices
|
||||||
|
|
20
app/assets/stylesheets/generic/tables.scss
Normal file
20
app/assets/stylesheets/generic/tables.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
table {
|
||||||
|
&.table {
|
||||||
|
tr {
|
||||||
|
td, th {
|
||||||
|
padding: 8px 10px;
|
||||||
|
line-height: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 15px;
|
||||||
|
border-bottom: 1px solid #CCC !important;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
border-color: #F1F1F1 !important;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,19 +17,6 @@
|
||||||
@include border-radius(0);
|
@include border-radius(0);
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
td, th {
|
|
||||||
padding: 8px 10px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 15px;
|
|
||||||
border-bottom: 1px solid #CCC !important;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
border-color: #F1F1F1 !important;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
td {
|
td {
|
||||||
background: $hover;
|
background: $hover;
|
||||||
|
|
41
app/controllers/oauth/applications_controller.rb
Normal file
41
app/controllers/oauth/applications_controller.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
||||||
|
before_filter :authenticate_user!
|
||||||
|
layout "profile"
|
||||||
|
|
||||||
|
def index
|
||||||
|
head :forbidden and return
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@application = Doorkeeper::Application.new(application_params)
|
||||||
|
|
||||||
|
if Doorkeeper.configuration.confirm_application_owner?
|
||||||
|
@application.owner = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
if @application.save
|
||||||
|
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
|
||||||
|
redirect_to oauth_application_url(@application)
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
if @application.destroy
|
||||||
|
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to applications_profile_url
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_application
|
||||||
|
@application = current_user.oauth_applications.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotFound do |exception|
|
||||||
|
render "errors/not_found", layout: "errors", status: 404
|
||||||
|
end
|
||||||
|
end
|
57
app/controllers/oauth/authorizations_controller.rb
Normal file
57
app/controllers/oauth/authorizations_controller.rb
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||||
|
before_filter :authenticate_resource_owner!
|
||||||
|
layout "profile"
|
||||||
|
|
||||||
|
def new
|
||||||
|
if pre_auth.authorizable?
|
||||||
|
if skip_authorization? || matching_token?
|
||||||
|
auth = authorization.authorize
|
||||||
|
redirect_to auth.redirect_uri
|
||||||
|
else
|
||||||
|
render "doorkeeper/authorizations/new"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
render "doorkeeper/authorizations/error"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Handle raise invalid authorization
|
||||||
|
def create
|
||||||
|
redirect_or_render authorization.authorize
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
redirect_or_render authorization.deny
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def matching_token?
|
||||||
|
Doorkeeper::AccessToken.matching_token_for(pre_auth.client,
|
||||||
|
current_resource_owner.id,
|
||||||
|
pre_auth.scopes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_or_render(auth)
|
||||||
|
if auth.redirectable?
|
||||||
|
redirect_to auth.redirect_uri
|
||||||
|
else
|
||||||
|
render json: auth.body, status: auth.status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pre_auth
|
||||||
|
@pre_auth ||=
|
||||||
|
Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration,
|
||||||
|
server.client_via_uid,
|
||||||
|
params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorization
|
||||||
|
@authorization ||= strategy.request
|
||||||
|
end
|
||||||
|
|
||||||
|
def strategy
|
||||||
|
@strategy ||= server.authorization_request(pre_auth.response_type)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||||
|
layout "profile"
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner)
|
||||||
|
redirect_to applications_profile_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,6 +13,11 @@ class ProfilesController < ApplicationController
|
||||||
def design
|
def design
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def applications
|
||||||
|
@applications = current_user.oauth_applications
|
||||||
|
@authorized_tokens = current_user.oauth_authorized_tokens
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
user_params.except!(:email) if @user.ldap_user?
|
user_params.except!(:email) if @user.ldap_user?
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,10 @@ module TabHelper
|
||||||
# nav_link(controller: [:tree, :refs]) { "Hello" }
|
# nav_link(controller: [:tree, :refs]) { "Hello" }
|
||||||
# # => '<li class="active">Hello</li>'
|
# # => '<li class="active">Hello</li>'
|
||||||
#
|
#
|
||||||
|
# # Several paths
|
||||||
|
# nav_link(path: ['tree#show', 'profile#show']) { "Hello" }
|
||||||
|
# # => '<li class="active">Hello</li>'
|
||||||
|
#
|
||||||
# # Shorthand path
|
# # Shorthand path
|
||||||
# nav_link(path: 'tree#show') { "Hello" }
|
# nav_link(path: 'tree#show') { "Hello" }
|
||||||
# # => '<li class="active">Hello</li>'
|
# # => '<li class="active">Hello</li>'
|
||||||
|
@ -38,25 +42,7 @@ module TabHelper
|
||||||
#
|
#
|
||||||
# Returns a list item element String
|
# Returns a list item element String
|
||||||
def nav_link(options = {}, &block)
|
def nav_link(options = {}, &block)
|
||||||
if path = options.delete(:path)
|
klass = active_nav_link?(options) ? 'active' : ''
|
||||||
if path.respond_to?(:each)
|
|
||||||
c = path.map { |p| p.split('#').first }
|
|
||||||
a = path.map { |p| p.split('#').last }
|
|
||||||
else
|
|
||||||
c, a, _ = path.split('#')
|
|
||||||
end
|
|
||||||
else
|
|
||||||
c = options.delete(:controller)
|
|
||||||
a = options.delete(:action)
|
|
||||||
end
|
|
||||||
|
|
||||||
if c && a
|
|
||||||
# When given both options, make sure BOTH are active
|
|
||||||
klass = current_controller?(*c) && current_action?(*a) ? 'active' : ''
|
|
||||||
else
|
|
||||||
# Otherwise check EITHER option
|
|
||||||
klass = current_controller?(*c) || current_action?(*a) ? 'active' : ''
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add our custom class into the html_options, which may or may not exist
|
# Add our custom class into the html_options, which may or may not exist
|
||||||
# and which may or may not already have a :class key
|
# and which may or may not already have a :class key
|
||||||
|
@ -72,6 +58,34 @@ module TabHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def active_nav_link?(options)
|
||||||
|
if path = options.delete(:path)
|
||||||
|
unless path.respond_to?(:each)
|
||||||
|
path = [path]
|
||||||
|
end
|
||||||
|
|
||||||
|
path.any? do |single_path|
|
||||||
|
current_path?(single_path)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
c = options.delete(:controller)
|
||||||
|
a = options.delete(:action)
|
||||||
|
|
||||||
|
if c && a
|
||||||
|
# When given both options, make sure BOTH are true
|
||||||
|
current_controller?(*c) && current_action?(*a)
|
||||||
|
else
|
||||||
|
# Otherwise check EITHER option
|
||||||
|
current_controller?(*c) || current_action?(*a)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_path?(path)
|
||||||
|
c, a, _ = path.split('#')
|
||||||
|
current_controller?(c) && current_action?(a)
|
||||||
|
end
|
||||||
|
|
||||||
def project_tab_class
|
def project_tab_class
|
||||||
return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
|
return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
|
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
|
||||||
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
|
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
|
||||||
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
|
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
|
||||||
|
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -564,4 +565,8 @@ class User < ActiveRecord::Base
|
||||||
namespaces += masters_groups
|
namespaces += masters_groups
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def oauth_authorized_tokens
|
||||||
|
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
41
app/services/oauth2/access_token_validation_service.rb
Normal file
41
app/services/oauth2/access_token_validation_service.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
module Oauth2::AccessTokenValidationService
|
||||||
|
# Results:
|
||||||
|
VALID = :valid
|
||||||
|
EXPIRED = :expired
|
||||||
|
REVOKED = :revoked
|
||||||
|
INSUFFICIENT_SCOPE = :insufficient_scope
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def validate(token, scopes: [])
|
||||||
|
if token.expired?
|
||||||
|
return EXPIRED
|
||||||
|
|
||||||
|
elsif token.revoked?
|
||||||
|
return REVOKED
|
||||||
|
|
||||||
|
elsif !self.sufficent_scope?(token, scopes)
|
||||||
|
return INSUFFICIENT_SCOPE
|
||||||
|
|
||||||
|
else
|
||||||
|
return VALID
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
# True if the token's scope is a superset of required scopes,
|
||||||
|
# or the required scopes is empty.
|
||||||
|
def sufficent_scope?(token, scopes)
|
||||||
|
if scopes.blank?
|
||||||
|
# if no any scopes required, the scopes of token is sufficient.
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
# If there are scopes required, then check whether
|
||||||
|
# the set of authorized scopes is a superset of the set of required scopes
|
||||||
|
required_scopes = Set.new(scopes)
|
||||||
|
authorized_scopes = Set.new(token.scopes)
|
||||||
|
|
||||||
|
return authorized_scopes >= required_scopes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
app/views/doorkeeper/applications/_delete_form.html.haml
Normal file
4
app/views/doorkeeper/applications/_delete_form.html.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- submit_btn_css ||= 'btn btn-link btn-remove btn-small'
|
||||||
|
= form_tag oauth_application_path(application) do
|
||||||
|
%input{:name => "_method", :type => "hidden", :value => "delete"}/
|
||||||
|
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
|
24
app/views/doorkeeper/applications/_form.html.haml
Normal file
24
app/views/doorkeeper/applications/_form.html.haml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
|
||||||
|
- if application.errors.any?
|
||||||
|
.alert.alert-danger{"data-alert" => ""}
|
||||||
|
%p Whoops! Check your form for possible errors
|
||||||
|
= content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do
|
||||||
|
= f.label :name, class: 'col-sm-2 control-label'
|
||||||
|
.col-sm-10
|
||||||
|
= f.text_field :name, class: 'form-control'
|
||||||
|
= doorkeeper_errors_for application, :name
|
||||||
|
= content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do
|
||||||
|
= f.label :redirect_uri, class: 'col-sm-2 control-label'
|
||||||
|
.col-sm-10
|
||||||
|
= f.text_area :redirect_uri, class: 'form-control'
|
||||||
|
= doorkeeper_errors_for application, :redirect_uri
|
||||||
|
%span.help-block
|
||||||
|
Use one line per URI
|
||||||
|
- if Doorkeeper.configuration.native_redirect_uri
|
||||||
|
%span.help-block
|
||||||
|
Use
|
||||||
|
%code= Doorkeeper.configuration.native_redirect_uri
|
||||||
|
for local tests
|
||||||
|
.form-actions
|
||||||
|
= f.submit 'Submit', class: "btn btn-primary wide"
|
||||||
|
= link_to "Cancel", applications_profile_path, class: "btn btn-default"
|
2
app/views/doorkeeper/applications/edit.html.haml
Normal file
2
app/views/doorkeeper/applications/edit.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
%h3.page-title Edit application
|
||||||
|
= render 'form', application: @application
|
16
app/views/doorkeeper/applications/index.html.haml
Normal file
16
app/views/doorkeeper/applications/index.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
%h3.page-title Your applications
|
||||||
|
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||||
|
%table.table.table-striped
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th Name
|
||||||
|
%th Callback URL
|
||||||
|
%th
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
- @applications.each do |application|
|
||||||
|
%tr{:id => "application_#{application.id}"}
|
||||||
|
%td= link_to application.name, oauth_application_path(application)
|
||||||
|
%td= application.redirect_uri
|
||||||
|
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
|
||||||
|
%td= render 'delete_form', application: application
|
2
app/views/doorkeeper/applications/new.html.haml
Normal file
2
app/views/doorkeeper/applications/new.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
%h3.page-title New application
|
||||||
|
= render 'form', application: @application
|
26
app/views/doorkeeper/applications/show.html.haml
Normal file
26
app/views/doorkeeper/applications/show.html.haml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
%h3.page-title
|
||||||
|
Application: #{@application.name}
|
||||||
|
|
||||||
|
|
||||||
|
%table.table
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
Application Id
|
||||||
|
%td
|
||||||
|
%code#application_id= @application.uid
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
Secret:
|
||||||
|
%td
|
||||||
|
%code#secret= @application.secret
|
||||||
|
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
Callback url
|
||||||
|
%td
|
||||||
|
- @application.redirect_uri.split.each do |uri|
|
||||||
|
%div
|
||||||
|
%span.monospace= uri
|
||||||
|
.form-actions
|
||||||
|
= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
|
||||||
|
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
|
3
app/views/doorkeeper/authorizations/error.html.haml
Normal file
3
app/views/doorkeeper/authorizations/error.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
%h3.page-title An error has occurred
|
||||||
|
%main{:role => "main"}
|
||||||
|
%pre= @pre_auth.error_response.body[:error_description]
|
28
app/views/doorkeeper/authorizations/new.html.haml
Normal file
28
app/views/doorkeeper/authorizations/new.html.haml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
%h3.page-title Authorize required
|
||||||
|
%main{:role => "main"}
|
||||||
|
%p.h4
|
||||||
|
Authorize
|
||||||
|
%strong.text-info= @pre_auth.client.name
|
||||||
|
to use your account?
|
||||||
|
- if @pre_auth.scopes
|
||||||
|
#oauth-permissions
|
||||||
|
%p This application will be able to:
|
||||||
|
%ul.text-info
|
||||||
|
- @pre_auth.scopes.each do |scope|
|
||||||
|
%li= t scope, scope: [:doorkeeper, :scopes]
|
||||||
|
%hr/
|
||||||
|
.actions
|
||||||
|
= form_tag oauth_authorization_path, method: :post do
|
||||||
|
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||||
|
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||||
|
= hidden_field_tag :state, @pre_auth.state
|
||||||
|
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||||
|
= hidden_field_tag :scope, @pre_auth.scope
|
||||||
|
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
|
||||||
|
= form_tag oauth_authorization_path, method: :delete do
|
||||||
|
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||||
|
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||||
|
= hidden_field_tag :state, @pre_auth.state
|
||||||
|
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||||
|
= hidden_field_tag :scope, @pre_auth.scope
|
||||||
|
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
|
3
app/views/doorkeeper/authorizations/show.html.haml
Normal file
3
app/views/doorkeeper/authorizations/show.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
%h3.page-title Authorization code:
|
||||||
|
%main{:role => "main"}
|
||||||
|
%code#authorization_code= params[:code]
|
|
@ -0,0 +1,4 @@
|
||||||
|
- submit_btn_css ||= 'btn btn-link btn-remove'
|
||||||
|
= form_tag oauth_authorized_application_path(application) do
|
||||||
|
%input{:name => "_method", :type => "hidden", :value => "delete"}/
|
||||||
|
= submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-small'
|
16
app/views/doorkeeper/authorized_applications/index.html.haml
Normal file
16
app/views/doorkeeper/authorized_applications/index.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
%header.page-header
|
||||||
|
%h1 Your authorized applications
|
||||||
|
%main{:role => "main"}
|
||||||
|
%table.table.table-striped
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th Application
|
||||||
|
%th Created At
|
||||||
|
%th
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
- @applications.each do |application|
|
||||||
|
%tr
|
||||||
|
%td= application.name
|
||||||
|
%td= application.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
%td= render 'delete_form', application: application
|
22
app/views/layouts/doorkeeper/admin.html.haml
Normal file
22
app/views/layouts/doorkeeper/admin.html.haml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
!!!
|
||||||
|
%html
|
||||||
|
%head
|
||||||
|
%meta{:charset => "utf-8"}
|
||||||
|
%meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"}
|
||||||
|
%meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}
|
||||||
|
%title Doorkeeper
|
||||||
|
= stylesheet_link_tag "doorkeeper/admin/application"
|
||||||
|
= csrf_meta_tags
|
||||||
|
%body
|
||||||
|
.navbar.navbar-inverse.navbar-fixed-top{:role => "navigation"}
|
||||||
|
.container
|
||||||
|
.navbar-header
|
||||||
|
= link_to 'OAuth2 Provider', oauth_applications_path, class: 'navbar-brand'
|
||||||
|
%ul.nav.navbar-nav
|
||||||
|
= content_tag :li, class: "#{'active' if request.path == oauth_applications_path}" do
|
||||||
|
= link_to 'Applications', oauth_applications_path
|
||||||
|
.container
|
||||||
|
- if flash[:notice].present?
|
||||||
|
.alert.alert-info
|
||||||
|
= flash[:notice]
|
||||||
|
= yield
|
15
app/views/layouts/doorkeeper/application.html.haml
Normal file
15
app/views/layouts/doorkeeper/application.html.haml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
!!!
|
||||||
|
%html
|
||||||
|
%head
|
||||||
|
%title OAuth authorize required
|
||||||
|
%meta{:charset => "utf-8"}
|
||||||
|
%meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"}
|
||||||
|
%meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}
|
||||||
|
= stylesheet_link_tag "doorkeeper/application"
|
||||||
|
= csrf_meta_tags
|
||||||
|
%body
|
||||||
|
#container
|
||||||
|
- if flash[:notice].present?
|
||||||
|
.alert.alert-info
|
||||||
|
= flash[:notice]
|
||||||
|
= yield
|
|
@ -8,6 +8,11 @@
|
||||||
= link_to profile_account_path do
|
= link_to profile_account_path do
|
||||||
%i.fa.fa-gear
|
%i.fa.fa-gear
|
||||||
Account
|
Account
|
||||||
|
= nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new']) do
|
||||||
|
= link_to applications_profile_path do
|
||||||
|
%i.fa.fa-cloud
|
||||||
|
%span
|
||||||
|
Applications
|
||||||
= nav_link(controller: :emails) do
|
= nav_link(controller: :emails) do
|
||||||
= link_to profile_emails_path do
|
= link_to profile_emails_path do
|
||||||
%i.fa.fa-envelope-o
|
%i.fa.fa-envelope-o
|
||||||
|
|
|
@ -75,3 +75,4 @@
|
||||||
The following groups will be abandoned. You should transfer or remove them:
|
The following groups will be abandoned. You should transfer or remove them:
|
||||||
%strong #{current_user.solo_owned_groups.map(&:name).join(', ')}
|
%strong #{current_user.solo_owned_groups.map(&:name).join(', ')}
|
||||||
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
|
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
|
||||||
|
|
||||||
|
|
47
app/views/profiles/applications.html.haml
Normal file
47
app/views/profiles/applications.html.haml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
%h3.page-title
|
||||||
|
OAuth2
|
||||||
|
|
||||||
|
%fieldset.oauth-applications
|
||||||
|
%legend Your applications
|
||||||
|
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||||
|
- if @applications.any?
|
||||||
|
%table.table.table-striped
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th Name
|
||||||
|
%th Callback URL
|
||||||
|
%th Clients
|
||||||
|
%th
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
- @applications.each do |application|
|
||||||
|
%tr{:id => "application_#{application.id}"}
|
||||||
|
%td= link_to application.name, oauth_application_path(application)
|
||||||
|
%td
|
||||||
|
- application.redirect_uri.split.each do |uri|
|
||||||
|
%div= uri
|
||||||
|
%td= application.access_tokens.count
|
||||||
|
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-small'
|
||||||
|
%td= render 'doorkeeper/applications/delete_form', application: application
|
||||||
|
|
||||||
|
%fieldset.oauth-authorized-applications.prepend-top-20
|
||||||
|
%legend Authorized applications
|
||||||
|
|
||||||
|
- if @authorized_tokens.any?
|
||||||
|
%table.table.table-striped
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th Name
|
||||||
|
%th Authorized At
|
||||||
|
%th Scope
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
- @authorized_tokens.each do |token|
|
||||||
|
- application = token.application
|
||||||
|
%tr{:id => "application_#{application.id}"}
|
||||||
|
%td= application.name
|
||||||
|
%td= token.created_at
|
||||||
|
%td= token.scopes
|
||||||
|
%td= render 'doorkeeper/authorized_applications/delete_form', application: application
|
||||||
|
- else
|
||||||
|
%p.light You dont have any authorized applications
|
91
config/initializers/doorkeeper.rb
Normal file
91
config/initializers/doorkeeper.rb
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
Doorkeeper.configure do
|
||||||
|
# Change the ORM that doorkeeper will use.
|
||||||
|
# Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper
|
||||||
|
orm :active_record
|
||||||
|
|
||||||
|
# This block will be called to check whether the resource owner is authenticated or not.
|
||||||
|
resource_owner_authenticator do
|
||||||
|
# Put your resource owner authentication logic here.
|
||||||
|
# Example implementation:
|
||||||
|
current_user || redirect_to(new_user_session_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
|
||||||
|
# admin_authenticator do
|
||||||
|
# # Put your admin authentication logic here.
|
||||||
|
# # Example implementation:
|
||||||
|
# Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Authorization Code expiration time (default 10 minutes).
|
||||||
|
# authorization_code_expires_in 10.minutes
|
||||||
|
|
||||||
|
# Access token expiration time (default 2 hours).
|
||||||
|
# If you want to disable expiration, set this to nil.
|
||||||
|
# access_token_expires_in 2.hours
|
||||||
|
|
||||||
|
# Reuse access token for the same resource owner within an application (disabled by default)
|
||||||
|
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
|
||||||
|
# reuse_access_token
|
||||||
|
|
||||||
|
# Issue access tokens with refresh token (disabled by default)
|
||||||
|
use_refresh_token
|
||||||
|
|
||||||
|
# Provide support for an owner to be assigned to each registered application (disabled by default)
|
||||||
|
# Optional parameter :confirmation => true (default false) if you want to enforce ownership of
|
||||||
|
# a registered application
|
||||||
|
# Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support
|
||||||
|
enable_application_owner :confirmation => true
|
||||||
|
|
||||||
|
# Define access token scopes for your provider
|
||||||
|
# For more information go to
|
||||||
|
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
|
||||||
|
default_scopes :api
|
||||||
|
#optional_scopes :write, :update
|
||||||
|
|
||||||
|
# Change the way client credentials are retrieved from the request object.
|
||||||
|
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||||
|
# falls back to the `:client_id` and `:client_secret` params from the `params` object.
|
||||||
|
# Check out the wiki for more information on customization
|
||||||
|
# client_credentials :from_basic, :from_params
|
||||||
|
|
||||||
|
# Change the way access token is authenticated from the request object.
|
||||||
|
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||||
|
# falls back to the `:access_token` or `:bearer_token` params from the `params` object.
|
||||||
|
# Check out the wiki for more information on customization
|
||||||
|
access_token_methods :from_access_token_param, :from_bearer_authorization, :from_bearer_param
|
||||||
|
|
||||||
|
# Change the native redirect uri for client apps
|
||||||
|
# When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider
|
||||||
|
# The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL
|
||||||
|
# (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi)
|
||||||
|
#
|
||||||
|
native_redirect_uri nil#'urn:ietf:wg:oauth:2.0:oob'
|
||||||
|
|
||||||
|
# Specify what grant flows are enabled in array of Strings. The valid
|
||||||
|
# strings and the flows they enable are:
|
||||||
|
#
|
||||||
|
# "authorization_code" => Authorization Code Grant Flow
|
||||||
|
# "implicit" => Implicit Grant Flow
|
||||||
|
# "password" => Resource Owner Password Credentials Grant Flow
|
||||||
|
# "client_credentials" => Client Credentials Grant Flow
|
||||||
|
#
|
||||||
|
# If not specified, Doorkeeper enables all the four grant flows.
|
||||||
|
#
|
||||||
|
# grant_flows %w(authorization_code implicit password client_credentials)
|
||||||
|
|
||||||
|
# Under some circumstances you might want to have applications auto-approved,
|
||||||
|
# so that the user skips the authorization step.
|
||||||
|
# For example if dealing with trusted a application.
|
||||||
|
# skip_authorization do |resource_owner, client|
|
||||||
|
# client.superapp? or resource_owner.admin?
|
||||||
|
# end
|
||||||
|
|
||||||
|
# WWW-Authenticate Realm (default "Doorkeeper").
|
||||||
|
# realm "Doorkeeper"
|
||||||
|
|
||||||
|
# Allow dynamic query parameters (disabled by default)
|
||||||
|
# Some applications require dynamic query parameters on their request_uri
|
||||||
|
# set to true if you want this to be allowed
|
||||||
|
# wildcard_redirect_uri false
|
||||||
|
end
|
73
config/locales/doorkeeper.en.yml
Normal file
73
config/locales/doorkeeper.en.yml
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
en:
|
||||||
|
activerecord:
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
application:
|
||||||
|
attributes:
|
||||||
|
redirect_uri:
|
||||||
|
fragment_present: 'cannot contain a fragment.'
|
||||||
|
invalid_uri: 'must be a valid URI.'
|
||||||
|
relative_uri: 'must be an absolute URI.'
|
||||||
|
mongoid:
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
application:
|
||||||
|
attributes:
|
||||||
|
redirect_uri:
|
||||||
|
fragment_present: 'cannot contain a fragment.'
|
||||||
|
invalid_uri: 'must be a valid URI.'
|
||||||
|
relative_uri: 'must be an absolute URI.'
|
||||||
|
mongo_mapper:
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
application:
|
||||||
|
attributes:
|
||||||
|
redirect_uri:
|
||||||
|
fragment_present: 'cannot contain a fragment.'
|
||||||
|
invalid_uri: 'must be a valid URI.'
|
||||||
|
relative_uri: 'must be an absolute URI.'
|
||||||
|
doorkeeper:
|
||||||
|
errors:
|
||||||
|
messages:
|
||||||
|
# Common error messages
|
||||||
|
invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.'
|
||||||
|
invalid_redirect_uri: 'The redirect uri included is not valid.'
|
||||||
|
unauthorized_client: 'The client is not authorized to perform this request using this method.'
|
||||||
|
access_denied: 'The resource owner or authorization server denied the request.'
|
||||||
|
invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
|
||||||
|
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
|
||||||
|
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
|
||||||
|
|
||||||
|
#configuration error messages
|
||||||
|
credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
|
||||||
|
resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.'
|
||||||
|
|
||||||
|
# Access grant errors
|
||||||
|
unsupported_response_type: 'The authorization server does not support this response type.'
|
||||||
|
|
||||||
|
# Access token errors
|
||||||
|
invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'
|
||||||
|
invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
|
||||||
|
unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.'
|
||||||
|
|
||||||
|
# Password Access token errors
|
||||||
|
invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found'
|
||||||
|
|
||||||
|
invalid_token:
|
||||||
|
revoked: "The access token was revoked"
|
||||||
|
expired: "The access token expired"
|
||||||
|
unknown: "The access token is invalid"
|
||||||
|
scopes:
|
||||||
|
api: Access your API
|
||||||
|
|
||||||
|
flash:
|
||||||
|
applications:
|
||||||
|
create:
|
||||||
|
notice: 'Application created.'
|
||||||
|
destroy:
|
||||||
|
notice: 'Application deleted.'
|
||||||
|
update:
|
||||||
|
notice: 'Application updated.'
|
||||||
|
authorized_applications:
|
||||||
|
destroy:
|
||||||
|
notice: 'Application revoked.'
|
|
@ -2,6 +2,11 @@ require 'sidekiq/web'
|
||||||
require 'api/api'
|
require 'api/api'
|
||||||
|
|
||||||
Gitlab::Application.routes.draw do
|
Gitlab::Application.routes.draw do
|
||||||
|
use_doorkeeper do
|
||||||
|
controllers :applications => 'oauth/applications',
|
||||||
|
:authorized_applications => 'oauth/authorized_applications',
|
||||||
|
:authorizations => 'oauth/authorizations'
|
||||||
|
end
|
||||||
#
|
#
|
||||||
# Search
|
# Search
|
||||||
#
|
#
|
||||||
|
@ -113,6 +118,7 @@ Gitlab::Application.routes.draw do
|
||||||
member do
|
member do
|
||||||
get :history
|
get :history
|
||||||
get :design
|
get :design
|
||||||
|
get :applications
|
||||||
|
|
||||||
put :reset_private_token
|
put :reset_private_token
|
||||||
put :update_username
|
put :update_username
|
||||||
|
|
42
db/migrate/20141216155758_create_doorkeeper_tables.rb
Normal file
42
db/migrate/20141216155758_create_doorkeeper_tables.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
class CreateDoorkeeperTables < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :oauth_applications do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :uid, null: false
|
||||||
|
t.string :secret, null: false
|
||||||
|
t.text :redirect_uri, null: false
|
||||||
|
t.string :scopes, null: false, default: ''
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :oauth_applications, :uid, unique: true
|
||||||
|
|
||||||
|
create_table :oauth_access_grants do |t|
|
||||||
|
t.integer :resource_owner_id, null: false
|
||||||
|
t.integer :application_id, null: false
|
||||||
|
t.string :token, null: false
|
||||||
|
t.integer :expires_in, null: false
|
||||||
|
t.text :redirect_uri, null: false
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
t.datetime :revoked_at
|
||||||
|
t.string :scopes
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :oauth_access_grants, :token, unique: true
|
||||||
|
|
||||||
|
create_table :oauth_access_tokens do |t|
|
||||||
|
t.integer :resource_owner_id
|
||||||
|
t.integer :application_id
|
||||||
|
t.string :token, null: false
|
||||||
|
t.string :refresh_token
|
||||||
|
t.integer :expires_in
|
||||||
|
t.datetime :revoked_at
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
t.string :scopes
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :oauth_access_tokens, :token, unique: true
|
||||||
|
add_index :oauth_access_tokens, :resource_owner_id
|
||||||
|
add_index :oauth_access_tokens, :refresh_token, unique: true
|
||||||
|
end
|
||||||
|
end
|
7
db/migrate/20141217125223_add_owner_to_application.rb
Normal file
7
db/migrate/20141217125223_add_owner_to_application.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class AddOwnerToApplication < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :oauth_applications, :owner_id, :integer, null: true
|
||||||
|
add_column :oauth_applications, :owner_type, :string, null: true
|
||||||
|
add_index :oauth_applications, [:owner_id, :owner_type]
|
||||||
|
end
|
||||||
|
end
|
45
db/schema.rb
45
db/schema.rb
|
@ -11,7 +11,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20141205134006) do
|
ActiveRecord::Schema.define(version: 20141217125223) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -249,6 +249,49 @@ ActiveRecord::Schema.define(version: 20141205134006) do
|
||||||
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
|
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
|
||||||
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
|
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
|
||||||
|
|
||||||
|
create_table "oauth_access_grants", force: true do |t|
|
||||||
|
t.integer "resource_owner_id", null: false
|
||||||
|
t.integer "application_id", null: false
|
||||||
|
t.string "token", null: false
|
||||||
|
t.integer "expires_in", null: false
|
||||||
|
t.text "redirect_uri", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "revoked_at"
|
||||||
|
t.string "scopes"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
|
||||||
|
|
||||||
|
create_table "oauth_access_tokens", force: true do |t|
|
||||||
|
t.integer "resource_owner_id"
|
||||||
|
t.integer "application_id"
|
||||||
|
t.string "token", null: false
|
||||||
|
t.string "refresh_token"
|
||||||
|
t.integer "expires_in"
|
||||||
|
t.datetime "revoked_at"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.string "scopes"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
|
||||||
|
add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree
|
||||||
|
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
|
||||||
|
|
||||||
|
create_table "oauth_applications", force: true do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.string "uid", null: false
|
||||||
|
t.string "secret", null: false
|
||||||
|
t.text "redirect_uri", null: false
|
||||||
|
t.string "scopes", default: "", null: false
|
||||||
|
t.datetime "created_at"
|
||||||
|
t.datetime "updated_at"
|
||||||
|
t.integer "owner_id"
|
||||||
|
t.string "owner_type"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
|
||||||
|
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
|
||||||
|
|
||||||
create_table "projects", force: true do |t|
|
create_table "projects", force: true do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.string "path"
|
t.string "path"
|
||||||
|
|
|
@ -71,6 +71,20 @@ Feature: Profile
|
||||||
And I click on my profile picture
|
And I click on my profile picture
|
||||||
Then I should see my user page
|
Then I should see my user page
|
||||||
|
|
||||||
|
Scenario: I can manage application
|
||||||
|
Given I visit profile applications page
|
||||||
|
Then I click on new application button
|
||||||
|
And I should see application form
|
||||||
|
Then I fill application form out and submit
|
||||||
|
And I see application
|
||||||
|
Then I click edit
|
||||||
|
And I see edit application form
|
||||||
|
Then I change name of application and submit
|
||||||
|
And I see that application was changed
|
||||||
|
Then I visit profile applications page
|
||||||
|
And I click to remove application
|
||||||
|
Then I see that application is removed
|
||||||
|
|
||||||
@javascript
|
@javascript
|
||||||
Scenario: I change my application theme
|
Scenario: I change my application theme
|
||||||
Given I visit profile design page
|
Given I visit profile design page
|
||||||
|
|
|
@ -221,4 +221,54 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
||||||
step 'I should see groups I belong to' do
|
step 'I should see groups I belong to' do
|
||||||
page.should have_css('.profile-groups-avatars', visible: true)
|
page.should have_css('.profile-groups-avatars', visible: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
step 'I click on new application button' do
|
||||||
|
click_on 'New Application'
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I should see application form' do
|
||||||
|
page.should have_content "New application"
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I fill application form out and submit' do
|
||||||
|
fill_in :doorkeeper_application_name, with: 'test'
|
||||||
|
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
|
||||||
|
click_on "Submit"
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I see application' do
|
||||||
|
page.should have_content "Application: test"
|
||||||
|
page.should have_content "Application Id"
|
||||||
|
page.should have_content "Secret"
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I click edit' do
|
||||||
|
click_on "Edit"
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I see edit application form' do
|
||||||
|
page.should have_content "Edit application"
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I change name of application and submit' do
|
||||||
|
page.should have_content "Edit application"
|
||||||
|
fill_in :doorkeeper_application_name, with: 'test_changed'
|
||||||
|
click_on "Submit"
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I see that application was changed' do
|
||||||
|
page.should have_content "test_changed"
|
||||||
|
page.should have_content "Application Id"
|
||||||
|
page.should have_content "Secret"
|
||||||
|
end
|
||||||
|
|
||||||
|
step 'I click to remove application' do
|
||||||
|
within '.oauth-applications' do
|
||||||
|
click_on "Destroy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
step "I see that application is removed" do
|
||||||
|
page.find(".oauth-applications").should_not have_content "test_changed"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -94,6 +94,10 @@ module SharedPaths
|
||||||
visit profile_path
|
visit profile_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
step 'I visit profile applications page' do
|
||||||
|
visit applications_profile_path
|
||||||
|
end
|
||||||
|
|
||||||
step 'I visit profile password page' do
|
step 'I visit profile password page' do
|
||||||
visit edit_profile_password_path
|
visit edit_profile_password_path
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file}
|
||||||
|
|
||||||
module API
|
module API
|
||||||
class API < Grape::API
|
class API < Grape::API
|
||||||
|
include APIGuard
|
||||||
version 'v3', using: :path
|
version 'v3', using: :path
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotFound do
|
rescue_from ActiveRecord::RecordNotFound do
|
||||||
|
|
175
lib/api/api_guard.rb
Normal file
175
lib/api/api_guard.rb
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
# Guard API with OAuth 2.0 Access Token
|
||||||
|
|
||||||
|
require 'rack/oauth2'
|
||||||
|
|
||||||
|
module APIGuard
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do |base|
|
||||||
|
# OAuth2 Resource Server Authentication
|
||||||
|
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
|
||||||
|
# The authenticator only fetches the raw token string
|
||||||
|
|
||||||
|
# Must yield access token to store it in the env
|
||||||
|
request.access_token
|
||||||
|
end
|
||||||
|
|
||||||
|
helpers HelperMethods
|
||||||
|
|
||||||
|
install_error_responders(base)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper Methods for Grape Endpoint
|
||||||
|
module HelperMethods
|
||||||
|
# Invokes the doorkeeper guard.
|
||||||
|
#
|
||||||
|
# If token is presented and valid, then it sets @current_user.
|
||||||
|
#
|
||||||
|
# If the token does not have sufficient scopes to cover the requred scopes,
|
||||||
|
# then it raises InsufficientScopeError.
|
||||||
|
#
|
||||||
|
# If the token is expired, then it raises ExpiredError.
|
||||||
|
#
|
||||||
|
# If the token is revoked, then it raises RevokedError.
|
||||||
|
#
|
||||||
|
# If the token is not found (nil), then it raises TokenNotFoundError.
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
#
|
||||||
|
# scopes: (optional) scopes required for this guard.
|
||||||
|
# Defaults to empty array.
|
||||||
|
#
|
||||||
|
def doorkeeper_guard!(scopes: [])
|
||||||
|
if (access_token = find_access_token).nil?
|
||||||
|
raise TokenNotFoundError
|
||||||
|
|
||||||
|
else
|
||||||
|
case validate_access_token(access_token, scopes)
|
||||||
|
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
|
||||||
|
raise InsufficientScopeError.new(scopes)
|
||||||
|
|
||||||
|
when Oauth2::AccessTokenValidationService::EXPIRED
|
||||||
|
raise ExpiredError
|
||||||
|
|
||||||
|
when Oauth2::AccessTokenValidationService::REVOKED
|
||||||
|
raise RevokedError
|
||||||
|
|
||||||
|
when Oauth2::AccessTokenValidationService::VALID
|
||||||
|
@current_user = User.find(access_token.resource_owner_id)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def doorkeeper_guard(scopes: [])
|
||||||
|
if access_token = find_access_token
|
||||||
|
case validate_access_token(access_token, scopes)
|
||||||
|
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
|
||||||
|
raise InsufficientScopeError.new(scopes)
|
||||||
|
|
||||||
|
when Oauth2::AccessTokenValidationService::EXPIRED
|
||||||
|
raise ExpiredError
|
||||||
|
|
||||||
|
when Oauth2::AccessTokenValidationService::REVOKED
|
||||||
|
raise RevokedError
|
||||||
|
|
||||||
|
when Oauth2::AccessTokenValidationService::VALID
|
||||||
|
@current_user = User.find(access_token.resource_owner_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
@current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def find_access_token
|
||||||
|
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
|
||||||
|
end
|
||||||
|
|
||||||
|
def doorkeeper_request
|
||||||
|
@doorkeeper_request ||= ActionDispatch::Request.new(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_access_token(access_token, scopes)
|
||||||
|
Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
# Installs the doorkeeper guard on the whole Grape API endpoint.
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
#
|
||||||
|
# scopes: (optional) scopes required for this guard.
|
||||||
|
# Defaults to empty array.
|
||||||
|
#
|
||||||
|
def guard_all!(scopes: [])
|
||||||
|
before do
|
||||||
|
guard! scopes: scopes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def install_error_responders(base)
|
||||||
|
error_classes = [ MissingTokenError, TokenNotFoundError,
|
||||||
|
ExpiredError, RevokedError, InsufficientScopeError]
|
||||||
|
|
||||||
|
base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
|
||||||
|
end
|
||||||
|
|
||||||
|
def oauth2_bearer_token_error_handler
|
||||||
|
Proc.new {|e|
|
||||||
|
response = case e
|
||||||
|
when MissingTokenError
|
||||||
|
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
|
||||||
|
|
||||||
|
when TokenNotFoundError
|
||||||
|
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||||
|
:invalid_token,
|
||||||
|
"Bad Access Token.")
|
||||||
|
|
||||||
|
when ExpiredError
|
||||||
|
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||||
|
:invalid_token,
|
||||||
|
"Token is expired. You can either do re-authorization or token refresh.")
|
||||||
|
|
||||||
|
when RevokedError
|
||||||
|
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||||
|
:invalid_token,
|
||||||
|
"Token was revoked. You have to re-authorize from the user.")
|
||||||
|
|
||||||
|
when InsufficientScopeError
|
||||||
|
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
|
||||||
|
# does not include WWW-Authenticate header, which breaks the standard.
|
||||||
|
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
|
||||||
|
:insufficient_scope,
|
||||||
|
Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope],
|
||||||
|
{ :scope => e.scopes})
|
||||||
|
end
|
||||||
|
|
||||||
|
response.finish
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Exceptions
|
||||||
|
#
|
||||||
|
|
||||||
|
class MissingTokenError < StandardError; end
|
||||||
|
|
||||||
|
class TokenNotFoundError < StandardError; end
|
||||||
|
|
||||||
|
class ExpiredError < StandardError; end
|
||||||
|
|
||||||
|
class RevokedError < StandardError; end
|
||||||
|
|
||||||
|
class InsufficientScopeError < StandardError
|
||||||
|
attr_reader :scopes
|
||||||
|
def initialize(scopes)
|
||||||
|
@scopes = scopes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,7 +11,7 @@ module API
|
||||||
|
|
||||||
def current_user
|
def current_user
|
||||||
private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
|
private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
|
||||||
@current_user ||= User.find_by(authentication_token: private_token)
|
@current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
|
||||||
|
|
||||||
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
|
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -41,6 +41,7 @@ describe API, api: true do
|
||||||
describe ".current_user" do
|
describe ".current_user" do
|
||||||
it "should return nil for an invalid token" do
|
it "should return nil for an invalid token" do
|
||||||
env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
|
env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
|
||||||
|
self.class.any_instance.stub(:doorkeeper_guard){ false }
|
||||||
current_user.should be_nil
|
current_user.should be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
31
spec/requests/api/doorkeeper_access_spec.rb
Normal file
31
spec/requests/api/doorkeeper_access_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe API::API, api: true do
|
||||||
|
include ApiHelpers
|
||||||
|
|
||||||
|
let!(:user) { create(:user) }
|
||||||
|
let!(:application) { Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "https://app.com", :owner => user) }
|
||||||
|
let!(:token) { Doorkeeper::AccessToken.create! :application_id => application.id, :resource_owner_id => user.id }
|
||||||
|
|
||||||
|
|
||||||
|
describe "when unauthenticated" do
|
||||||
|
it "returns authentication success" do
|
||||||
|
get api("/user"), :access_token => token.token
|
||||||
|
response.status.should == 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when token invalid" do
|
||||||
|
it "returns authentication error" do
|
||||||
|
get api("/user"), :access_token => "123a"
|
||||||
|
response.status.should == 401
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "authorization by private token" do
|
||||||
|
it "returns authentication success" do
|
||||||
|
get api("/user", user)
|
||||||
|
response.status.should == 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue