Project

General

Profile

Feature #24808 » 0004-Add-OAuth2-provider-capability-using-doorkeeper-gem.patch

Jens Krämer, 2020-08-27 05:04

View differences:

Gemfile
18 18
gem "rbpdf", "~> 1.20.0"
19 19
gem 'addressable'
20 20
gem 'rubyzip', (RUBY_VERSION < '2.4' ? '~> 1.3.0' : '~> 2.3.0')
21
gem "doorkeeper", "~> 5.4"
22
gem "bcrypt", require: false
23
gem "doorkeeper-i18n", "~> 5.0"
21 24

  
22 25
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
23 26
gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin]
......
91 94
  gem 'rubocop', '~> 0.81.0'
92 95
  gem 'rubocop-performance', '~> 1.5.0'
93 96
  gem 'rubocop-rails', '~> 2.5.0'
97
  # for testing oauth provider capabilities
98
  gem 'oauth2'
99
  gem 'rest-client'
94 100
end
95 101

  
96 102
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
app/controllers/application_controller.rb
123 123
      if (key = api_key_from_request)
124 124
        # Use API key
125 125
        user = User.find_by_api_key(key)
126
      elsif access_token = Doorkeeper.authenticate(request)
127
        # Oauth
128
        if access_token.accessible?
129
          user = User.active.find_by_id(access_token.resource_owner_id)
130
          user.oauth_scope = access_token.scopes.all.map(&:to_sym)
131
        else
132
          doorkeeper_render_error
133
        end
126 134
      elsif /\ABasic /i.match?(request.authorization.to_s)
127 135
        # HTTP Basic, either username/password or API key/random
128 136
        authenticate_with_http_basic do |username, password|
app/controllers/oauth2_applications_controller.rb
1
class Oauth2ApplicationsController < Doorkeeper::ApplicationsController
2

  
3
  private
4

  
5
  def application_params
6
    params[:doorkeeper_application] ||= {}
7
    params[:doorkeeper_application][:scopes] ||= []
8

  
9
    scopes = Redmine::AccessControl.public_permissions.map{|p| p.name.to_s}
10

  
11
    if params[:doorkeeper_application][:scopes].is_a?(Array)
12
      scopes |= params[:doorkeeper_application][:scopes]
13
    else
14
      scopes |= params[:doorkeeper_application][:scopes].split(/\s+/)
15
    end
16
    params[:doorkeeper_application][:scopes] = scopes.join(' ')
17
    super
18
  end
19
end
app/models/user.rb
100 100
  attr_accessor :password, :password_confirmation, :generate_password
101 101
  attr_accessor :last_before_login_on
102 102
  attr_accessor :remote_ip
103
  attr_writer   :oauth_scope
103 104

  
104 105
  LOGIN_LENGTH_LIMIT = 60
105 106
  MAIL_LENGTH_LIMIT = 60
......
693 694
    end
694 695
  end
695 696

  
697
  def admin?
698
    if authorized_by_oauth?
699
      # when signed in via oauth, the user only acts as admin when the admin scope is set
700
      super and @oauth_scope.include?(:admin)
701
    else
702
      super
703
    end
704
  end
705

  
706
  # true if the user has signed in via oauth
707
  def authorized_by_oauth?
708
    !@oauth_scope.nil?
709
  end
710

  
696 711
  # Return true if the user is allowed to do the specified action on a specific context
697 712
  # Action can be:
698 713
  # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
......
713 728

  
714 729
      roles.any? do |role|
715 730
        (context.is_public? || role.member?) &&
716
        role.allowed_to?(action) &&
731
        role.allowed_to?(action, @oauth_scope) &&
717 732
        (block_given? ? yield(role, self) : true)
718 733
      end
719 734
    elsif context && context.is_a?(Array)
......
732 747
      # authorize if user has at least one role that has this permission
733 748
      roles = self.roles.to_a | [builtin_role]
734 749
      roles.any? do |role|
735
        role.allowed_to?(action) &&
750
        role.allowed_to?(action, @oauth_scope) &&
736 751
        (block_given? ? yield(role, self) : true)
737 752
      end
738 753
    else
app/views/doorkeeper/applications/_form.html.erb
1
<%= error_messages_for 'application' %>
2
<div class="box tabular">
3
  <p><%= f.text_field :name, :required => true %></p>
4

  
5
  <p>
6
    <%= f.text_area :redirect_uri, :required => true, :size => 60, :label => :'activerecord.attributes.doorkeeper/application.redirect_uri' %>
7
    <em class="info">
8
      <%= t('doorkeeper.applications.help.redirect_uri') %>
9
    </em>
10
  </p>
11
</div>
12

  
13
<h3><%= l(:'activerecord.attributes.doorkeeper/application.scopes') %></h3>
14
<p><em class="info"><%= l :text_oauth_info_scopes %></em></p>
15
<div class="box tabular" id="scopes">
16
<fieldset><legend><%= l :label_oauth_admin_access %></legend>
17
  <label class="floating" style="width: auto;">
18
    <%= check_box_tag 'doorkeeper_application[scopes][]', 'admin', @application.scopes.include?('admin'),
19
      :id => "doorkeeper_application_scopes_admin"
20
    %>
21
    <%= l :text_oauth_admin_permission %>
22
  </label>
23
</fieldset>
24
<% perms_by_module = Redmine::AccessControl.permissions.group_by {|p| p.project_module.to_s} %>
25
<% perms_by_module.keys.sort.each do |mod| %>
26
    <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
27
    <% perms_by_module[mod].each do |permission| %>
28
        <label class="floating">
29
        <%= check_box_tag 'doorkeeper_application[scopes][]', permission.name.to_s, (permission.public? || @application.scopes.include?( permission.name.to_s)),
30
              :id => "doorkeeper_application_scopes_#{permission.name}",
31
              :disabled => permission.public? %>
32
        <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
33
        </label>
34
    <% end %>
35
    </fieldset>
36
<% end %>
37
<br /><%= check_all_links 'scopes' %>
38
<%= hidden_field_tag 'doorkeeper_application[scopes][]', '' %>
39
</div>
app/views/doorkeeper/applications/edit.html.erb
1
<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
2

  
3
<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %>
4
  <%= render :partial => 'form', :locals => {:f => f} %>
5
  <%= submit_tag l(:button_save) %>
6
<% end %>
app/views/doorkeeper/applications/index.html.erb
1
<div class="contextual">
2
<%= link_to t('.new'), new_oauth_application_path, :class => 'icon icon-add' %>
3
</div>
4

  
5
<%= title l 'label_oauth_application_plural' %>
6

  
7
<% if @applications.any? %>
8
<div class="autoscroll">
9
<table class="list">
10
  <thead><tr>
11
    <th><%= t('.name') %></th>
12
    <th><%= t('.callback_url') %></th>
13
    <th><%= t('.scopes') %></th>
14
    <th></th>
15
  </tr></thead>
16
  <tbody>
17
  <% @applications.each do |application| %>
18
    <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
19
      <td class="name"><span><%= link_to application.name, oauth_application_path(application) %></span></td>
20
      <td class="description"><%= truncate application.redirect_uri.split.join(', '), length: 50 %></td>
21
      <td class="description"><%= safe_join application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></td>
22
      <td class="buttons">
23
        <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'icon icon-edit' %>
24
        <%= link_to t('doorkeeper.applications.buttons.destroy'), oauth_application_path(application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
25
      </td>
26
    </tr>
27
  <% end %>
28
  </tbody>
29
</table>
30
</div>
31
<% else %>
32
  <p class="nodata"><%= l(:label_no_data) %></p>
33
<% end %>
app/views/doorkeeper/applications/new.html.erb
1
<%= title [l('label_oauth_application_plural'), oauth_applications_path], t('.title') %>
2

  
3
<%= labelled_form_for @application, url: doorkeeper_submit_path(@application)  do |f| %>
4
<%= render :partial => 'form', :locals => { :f => f } %>
5
<%= submit_tag l(:button_create) %>
6
<% end %>
app/views/doorkeeper/applications/show.html.erb
1
<div class="contextual">
2
<%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(@application), :accesskey => accesskey(:edit), class: 'icon icon-edit' %>
3
<%= link_to t('doorkeeper.applications.buttons.destroy'), oauth_application_path(@application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
4
</div>
5

  
6
<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
7

  
8
<div class="box">
9
  <h3 class="icon icon-passwd"><%= l(:label_information_plural) %></h3>
10
  <p>
11
    <span class="label"><%= t('.application_id') %>:</span>
12
    <code><%= h @application.uid %></code>
13
  </p>
14
  <p>
15
    <span class="label"><%= t('.secret') %>:</span>
16
    <code>
17
      <% secret = flash[:application_secret].presence || @application.plaintext_secret %>
18
      <% flash.delete :application_secret %>
19
      <% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
20
        <%= t('.secret_hashed') %>
21
      <% else %>
22
        <%= secret %>
23
      <% end %>
24
    </code>
25
    <% if secret.present? && Doorkeeper.config.application_secret_hashed? %>
26
       <strong><%= t "text_oauth_copy_secret_now" %></strong>
27
    <% end %>
28
  </p>
29
  <p>
30
    <span class="label"><%= t('.scopes') %>:</span>
31
    <code><%= safe_join @application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></code>
32
  </p>
33
</div>
34

  
35
<h3><%= t('.callback_urls') %></h3>
36

  
37
<div class="autoscroll">
38
<table class="list">
39
  <thead><tr>
40
    <th><%= t('.callback_url') %></th>
41
    <th></th>
42
  </tr></thead>
43
  <tbody>
44
  <% @application.redirect_uri.split.each do |uri| %>
45
    <tr class="<%= cycle("odd", "even") %>">
46
      <td class="name"><span><%= uri %></span></td>
47
      <td class="buttons">
48
        <%= link_to t('doorkeeper.applications.buttons.authorize'), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'icon icon-authorize', target: '_blank' %>
49
      </td>
50
    </tr>
51
  <% end %>
52
  </tbody>
53
</table>
54
</div>
app/views/doorkeeper/authorizations/error.html.erb
1
<h2><%= t('doorkeeper.authorizations.error.title') %></h2>
2

  
3
<p id="errorExplanation"><%= @pre_auth.error_response.body[:error_description] %></p>
4
<p><a href="javascript:history.back()"><%= l(:button_back) %></a></p>
5

  
6
<% html_title t('doorkeeper.authorizations.error.title') %>
app/views/doorkeeper/authorizations/new.html.erb
1
<%= title t('.title') %>
2

  
3
<div class="warning">
4
<p><strong><%=h @pre_auth.client.name %></strong></p>
5

  
6
<p><%= raw t('.prompt', client_name: content_tag(:strong, class: "text-info") { @pre_auth.client.name }) %></p>
7

  
8
<div class="oauth-permissions">
9
  <p><%= t('.able_to') %>:</p>
10
  <ul>
11
    <li><%= l :text_oauth_implicit_permissions %></li>
12
    <% @pre_auth.scopes.each do |scope| %>
13
      <% if scope == 'admin' %>
14
        <li><%= l :label_oauth_permission_admin %></li>
15
      <% else %>
16
        <li><%= l_or_humanize(scope, prefix: 'permission_') %></li>
17
      <% end %>
18
    <% end %>
19
  </ul>
20
</div>
21

  
22
<% if @pre_auth.scopes.include?('admin') %>
23
  <p><%= l :text_oauth_admin_permission_info %></p>
24
<% end %>
25
</div>
26

  
27
<p>
28
  <%= form_tag oauth_authorization_path, method: :post do %>
29
    <%= hidden_field_tag :client_id, @pre_auth.client.uid %>
30
    <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
31
    <%= hidden_field_tag :state, @pre_auth.state %>
32
    <%= hidden_field_tag :response_type, @pre_auth.response_type %>
33
    <%= hidden_field_tag :scope, @pre_auth.scope %>
34
    <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
35
    <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
36
    <%= submit_tag t('doorkeeper.authorizations.buttons.authorize') %>
37
  <% end %>
38
  <%= form_tag oauth_authorization_path, method: :delete do %>
39
    <%= hidden_field_tag :client_id, @pre_auth.client.uid %>
40
    <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
41
    <%= hidden_field_tag :state, @pre_auth.state %>
42
    <%= hidden_field_tag :response_type, @pre_auth.response_type %>
43
    <%= hidden_field_tag :scope, @pre_auth.scope %>
44
    <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
45
    <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
46
    <%= submit_tag t('doorkeeper.authorizations.buttons.deny') %>
47
  <% end %>
48
</p>
app/views/doorkeeper/authorizations/show.html.erb
1
<%= title [l('label_oauth_authorized_application_plural'), oauth_authorized_applications_path]  %>
2

  
3
<fieldset class="tabular"><legend><%= l(:label_information_plural) %></legend>
4
  <p>
5
    <label><%= t('.title') %>:</label>
6
    <code><%= params[:code] %></code>
7
  </p>
8
</fieldset>
app/views/doorkeeper/authorized_applications/index.html.erb
1
<%= title [t(:label_my_account), my_account_path], l('label_oauth_authorized_application_plural') %>
2

  
3
<% if @applications.any? %>
4
<div class="autoscroll">
5
<table class="list">
6
  <thead><tr>
7
    <th><%= t('doorkeeper.authorized_applications.index.application') %></th>
8
    <th><%= t('doorkeeper.authorized_applications.index.created_at') %></th>
9
    <th></th>
10
  </tr></thead>
11
  <tbody>
12
  <% @applications.each do |application| %>
13
    <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
14
      <td class="name"><span><%= application.name %></span></td>
15
      <td ><%= format_date application.created_at %></td>
16
      <td class="buttons">
17
        <%= link_to t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), :data => {:confirm => t('doorkeeper.authorized_applications.confirmations.revoke')}, :method => :delete, :class => 'icon icon-del' %>
18
      </td>
19
    </tr>
20
  <% end %>
21
  </tbody>
22
</table>
23
</div>
24
<% else %>
25
  <p class="nodata"><%= l(:label_no_data) %></p>
26
<% end %>
27

  
28
<% content_for :sidebar do %>
29
<% @user = User.current %>
30
<%= render :partial => 'my/sidebar' %>
31
<% end %>
app/views/my/account.html.erb
1 1
<div class="contextual">
2 2
<%= additional_emails_link(@user) %>
3 3
<%= link_to(l(:button_change_password), {:action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
4
<%= link_to(l('label_oauth_authorized_application_plural'), oauth_authorized_applications_path, :class => 'icon icon-applications') if Setting.rest_api_enabled? %>
4 5
<%= call_hook(:view_my_account_contextual, :user => @user)%>
5 6
</div>
6 7

  
app/views/users/show.api.rsb
9 9
  api.updated_on @user.updated_on
10 10
  api.last_login_on     @user.last_login_on
11 11
  api.passwd_changed_on @user.passwd_changed_on
12
  api.api_key    @user.api_key if User.current.admin? || (User.current == @user)
12
  api.api_key    @user.api_key if (User.current.admin? || (User.current == @user && !User.current.authorized_by_oauth?))
13 13
  api.status     @user.status if User.current.admin?
14 14

  
15 15
  render_api_custom_values @user.visible_custom_field_values, api
config/application.rb
81 81
      :key => '_redmine_session',
82 82
      :path => config.relative_url_root || '/'
83 83

  
84
    # Use Redmine standard layouts and helpers for Doorkeeper OAuth2 screens
85
    config.to_prepare do
86
      Doorkeeper::ApplicationsController.layout "admin"
87
      Doorkeeper::ApplicationsController.main_menu = false
88
      Doorkeeper::AuthorizationsController.layout "base"
89
      Doorkeeper::AuthorizedApplicationsController.layout "base"
90
      Doorkeeper::AuthorizedApplicationsController.main_menu = false
91
    end
92

  
84 93
    if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
85 94
      instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
86 95
    end
config/initializers/doorkeeper.rb
1
Doorkeeper.configure do
2
  orm :active_record
3

  
4
  # Issue access tokens with refresh token
5
  use_refresh_token
6

  
7
  # Authorization Code expiration time (default: 10 minutes).
8
  #
9
  # authorization_code_expires_in 10.minutes
10

  
11
  # Access token expiration time (default: 2 hours).
12
  # If you want to disable expiration, set this to `nil`.
13
  #
14
  # access_token_expires_in 2.hours
15

  
16

  
17
  # Hash access and refresh tokens before persisting them.
18
  # https://doorkeeper.gitbook.io/guides/security/token-and-application-secrets
19
  hash_token_secrets
20

  
21
  # Hash application secrets before persisting them.
22
  hash_application_secrets  using: '::Doorkeeper::SecretStoring::BCrypt'
23

  
24
  # limit supported flows to Auth code
25
  grant_flows ['authorization_code']
26

  
27
  realm           Redmine::Info.app_name
28
  base_controller 'ApplicationController'
29
  default_scopes  *Redmine::AccessControl.public_permissions.map(&:name)
30
  optional_scopes *(Redmine::AccessControl.permissions.map(&:name) << :admin)
31

  
32
  # Forbids creating/updating applications with arbitrary scopes that are
33
  # not in configuration, i.e. +default_scopes+ or +optional_scopes+.
34
  enforce_configured_scopes
35

  
36
  allow_token_introspection false
37

  
38
  # allow http loopback redirect URIs but require https for all others
39
  force_ssl_in_redirect_uri { |uri| !%w[localhost 127.0.0.1].include?(uri.host) }
40

  
41
  # Specify what redirect URI's you want to block during Application creation.
42
  forbid_redirect_uri { |uri| %w[data vbscript javascript].include?(uri.scheme.to_s.downcase) }
43

  
44

  
45
  resource_owner_authenticator do
46
    if require_login
47
      if Setting.rest_api_enabled?
48
        User.current
49
      else
50
        deny_access
51
      end
52
    end
53
  end
54

  
55
  admin_authenticator do
56
    if !Setting.rest_api_enabled? || !User.current.admin?
57
      deny_access
58
    end
59
  end
60
end
61

  
62
Doorkeeper::ApplicationsController.class_eval do
63
  require_sudo_mode :create, :show, :update, :destroy
64
end
65
Doorkeeper::AuthorizationsController.class_eval do
66
  require_sudo_mode :create, :destroy
67
end
config/locales/de.yml
963 963
  permission_view_time_entries: Gebuchte Aufwände ansehen
964 964
  permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen
965 965
  permission_view_wiki_pages: Wiki ansehen
966
  permission_view_project: Projekte ansehen
967
  permission_search_project: Projekte suchen
968
  permission_view_members: Projektmitglieder anzeigen
966 969

  
967 970
  project_module_boards: Foren
968 971
  project_module_calendar: Kalender
......
1321 1324
  field_passwd_changed_on: Password last changed
1322 1325
  label_import_users: Import users
1323 1326
  label_days_to_html: "%{days} days up to %{date}"
1327
  label_oauth_permission_admin: Administrator-Zugriff
1328
  label_oauth_admin_access: Administration
1329
  label_oauth_application_plural: Applikationen
1330
  label_oauth_authorized_application_plural: Autorisierte Applikationen
1331
  text_oauth_admin_permission: Voller Admin-Zugriff. Wenn diese Applikation durch einen Administrator autorisiert wird, kann sie alle Daten lesen und schreiben, auch im Namen anderer Benutzer.
1332
  text_oauth_admin_permission_info: Diese Applikation verlangt vollen Administrator-Zugriff. Wenn Sie ein Administrator sind (oder in Zukunft Administrator werden), wird sie in der Lage sein, alle Daten zu lesen und zu schreiben, auch im Namen anderer Benutzer. Dies kann vermieden werden, indem die Applikation mit einem anderen Benutzerkonto ohne Administrator-Privileg autorisiert wird.
1333
  text_oauth_copy_secret_now: Das Geheimnis bitte jetzt an einen sicheren Ort kopieren, es kann nicht erneut angezeigt werden.
1334
  text_oauth_implicit_permissions: Zugriff auf Benutzername, Login sowie auf die primäre Email-Adresse
1335
  text_oauth_info_scopes: Scopes für die Applikation auswählen. Die Applikation wird niemals mehr Rechte haben als hier ausgewählt. Sie wird außerdem auf die Rollen und Projektmitgliedschaften des Benutzers, der sie autorisiert hat, beschränkt sein.
config/locales/en.yml
562 562
  permission_manage_related_issues: Manage related issues
563 563
  permission_import_issues: Import issues
564 564
  permission_log_time_for_other_users: Log spent time for other users
565
  permission_view_project: View projects
566
  permission_search_project: Search projects
567
  permission_view_members: View project members
568

  
565 569

  
566 570
  project_module_issue_tracking: Issue tracking
567 571
  project_module_time_tracking: Time tracking
......
1297 1301
  text_project_is_public_anonymous: Public projects and their contents are openly available on the network.
1298 1302
  label_import_time_entries: Import time entries
1299 1303
  label_import_users: Import users
1304
  label_oauth_permission_admin: Administrate this Redmine
1305
  label_oauth_admin_access: Administration
1306
  label_oauth_application_plural: Applications
1307
  label_oauth_authorized_application_plural: Authorized applications
1308
  text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, this application will be able to read and write all data and impersonate other users.
1309
  text_oauth_admin_permission_info: This application requests full administrative access. If you are an Administrator (or become one in the future), it will be able to read and write all data and impersonate other users on your behalf. If you want to avoid this, authorize it as a user without Administrator privileges instead.
1310
  text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown again.
1311
  text_oauth_implicit_permissions: View your name, login and primary email address
1312
  text_oauth_info_scopes: Select the scopes this application may request. The application will not be allowed to do more than what is selected here. It will also always be limited by the roles and project memberships of the user who authorized it.
config/routes.rb
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20 20
Rails.application.routes.draw do
21

  
22
  use_doorkeeper do
23
    controllers :applications => 'oauth2_applications'
24
  end
25

  
26
  root :to => 'welcome#index'
21 27
  root :to => 'welcome#index', :as => 'home'
22 28

  
23 29
  match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
db/migrate/20170107092155_create_doorkeeper_tables.rb
1
class CreateDoorkeeperTables < ActiveRecord::Migration[4.2]
2
  def change
3
    create_table :oauth_applications do |t|
4
      t.string  :name,         null: false
5
      t.string  :uid,          null: false
6
      t.string  :secret,       null: false
7
      t.text    :redirect_uri, null: false
8
      t.text    :scopes,       null: false
9
      t.boolean :confidential, null: false, default: true
10
      t.timestamps             null: false
11
    end
12

  
13
    add_index :oauth_applications, :uid, unique: true
14

  
15
    create_table :oauth_access_grants do |t|
16
      t.integer  :resource_owner_id, null: false
17
      t.references :application,     null: false
18
      t.string   :token,             null: false
19
      t.integer  :expires_in,        null: false
20
      t.text     :redirect_uri,      null: false
21
      t.datetime :created_at,        null: false
22
      t.datetime :revoked_at
23
      t.text     :scopes,            null: false, default: ''
24
    end
25

  
26
    add_index :oauth_access_grants, :token, unique: true
27
    add_foreign_key(
28
      :oauth_access_grants,
29
      :oauth_applications,
30
      column: :application_id
31
    )
32
    add_foreign_key(
33
      :oauth_access_grants,
34
      :users,
35
      column: :resource_owner_id
36
    )
37

  
38
    create_table :oauth_access_tokens do |t|
39
      t.integer  :resource_owner_id
40
      t.references :application
41

  
42
      t.string   :token,                  null: false
43

  
44
      t.string   :refresh_token
45
      t.integer  :expires_in
46
      t.datetime :revoked_at
47
      t.datetime :created_at,             null: false
48
      t.text     :scopes
49

  
50
      t.string   :previous_refresh_token, null: false, default: ""
51
    end
52

  
53
    add_index :oauth_access_tokens, :token, unique: true
54
    add_index :oauth_access_tokens, :resource_owner_id
55
    add_index :oauth_access_tokens, :refresh_token, unique: true
56

  
57
    add_foreign_key(
58
      :oauth_access_tokens,
59
      :oauth_applications,
60
      column: :application_id
61
    )
62
    add_foreign_key(
63
      :oauth_access_tokens,
64
      :users,
65
      column: :resource_owner_id
66
    )
67
  end
68
end
db/migrate/20200812065227_enable_pkce.rb
1
# frozen_string_literal: true
2

  
3
class EnablePkce < ActiveRecord::Migration[5.2]
4
  def change
5
    add_column :oauth_access_grants, :code_challenge, :string, null: true
6
    add_column :oauth_access_grants, :code_challenge_method, :string, null: true
7
  end
8
end
lib/redmine.rb
267 267
            :html => {:class => 'icon icon-settings'}
268 268
  menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
269 269
            :html => {:class => 'icon icon-server-authentication'}
270
  menu.push :applications, {:controller => 'oauth2_applications', :action => 'index'},
271
            :if => Proc.new { Setting.rest_api_enabled? },
272
            :caption => :'doorkeeper.layouts.admin.nav.applications',
273
            :html => {:class => 'icon icon-applications'}
270 274
  menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true,
271 275
            :html => {:class => 'icon icon-plugins'}
272 276
  menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true,
public/stylesheets/application.css
1022 1022
  color: #A6750C;
1023 1023
}
1024 1024

  
1025
.warning .oauth-permissions { display:inline-block;text-align:left; }
1026
.warning .oauth-permissions p { margin-top:0;-webkit-margin-before:0;}
1027

  
1025 1028
#errorExplanation ul { font-size: 0.9em;}
1026 1029
#errorExplanation h2, #errorExplanation p { display: none; }
1027 1030

  
......
1544 1547
.icon-workflows { background-image: url(../images/ticket_go.png); }
1545 1548
.icon-custom-fields { background-image: url(../images/textfield.png); }
1546 1549
.icon-plugins { background-image: url(../images/plugin.png); }
1550
.icon-applications { background-image: url(../images/application_view_tile.png); }
1551
.icon-authorize { background-image: url(../images/application_key.png); }
1547 1552
.icon-news { background-image: url(../images/news.png); }
1548 1553
.icon-issue-closed { background-image: url(../images/ticket_checked.png); }
1549 1554
.icon-issue-note { background-image: url(../images/ticket_note.png); }
test/system/oauth_provider_test.rb
1
# frozen_string_literal: true
2

  
3
require File.expand_path('../../application_system_test_case', __FILE__)
4
require 'oauth2'
5
require 'webrick'
6

  
7

  
8
class OauthProviderSystemTest < ApplicationSystemTestCase
9

  
10
  fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
11
           :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues,
12
           :enumerations, :custom_fields, :custom_values, :custom_fields_trackers,
13
           :watchers, :journals, :journal_details, :versions,
14
           :workflows
15

  
16

  
17
  test 'application creation and authorization' do
18
    #
19
    # admin creates the application, granting permissions and generating a uuid
20
    # and secret.
21
    #
22
    log_user 'admin', 'admin'
23
    with_settings rest_api_enabled: 1 do
24
      visit '/admin'
25
      within 'div#admin-menu ul' do
26
        click_link 'Applications'
27
      end
28
      click_link 'New Application'
29
      fill_in 'Name', with: 'Oauth Test'
30

  
31
      # as per https://tools.ietf.org/html/rfc8252#section-7.3, the port can be
32
      # anything when the redirect URI's host is 127.0.0.1.
33
      fill_in 'Redirect URI', with: 'http://127.0.0.1'
34

  
35
      find('#doorkeeper_application_scopes_view_issues').set(true)
36
      click_button 'Create'
37
    end
38

  
39
    assert app = Doorkeeper::Application.find_by_name('Oauth Test')
40

  
41
    find 'h2', visible: true, text: /Oauth Test/
42
    find 'p code', visible: true, text: app.uid
43
    find 'p strong', visible: true, text: /will not be shown again/
44
    find 'p code', visible: true, text: /View Issues/
45

  
46
    # scrape the clear text secret from the page
47
    app_secret = all(:css, 'p code')[1].text
48

  
49
    click_link 'Sign out'
50

  
51

  
52
    #
53
    # regular user authorizes the application
54
    #
55

  
56
    client = OAuth2::Client.new(app.uid, app_secret, site: "http://127.0.0.1:#{test_port}/")
57

  
58
    # set up a dummy http listener to handle the redirect
59
    port = (rand(10000)+10000)
60
    redirect_uri = "http://127.0.0.1:#{port}"
61
    # the request handler below will set this to the auth token
62
    token = nil
63

  
64
    # launches webrick, listening for the redirect with the auth code.
65
    launch_client_app(port: port) do |req,res|
66
      # get access code from code url param
67
      if code = req.query['code'].presence
68
        # exchange it for token
69
        token = client.auth_code.get_token(code, redirect_uri: redirect_uri)
70
        res.body = "<html><body><p>Authorization succeeded, you may close this window now.</p></body></html>"
71
      end
72
    end
73

  
74
    log_user 'jsmith', 'jsmith'
75
    with_settings rest_api_enabled: 1 do
76
      visit '/my/account'
77
      click_link 'Authorized applications'
78
      find 'p.nodata', visible: true
79

  
80
      # an oauth client would send the user to this url to request permission
81
      url = client.auth_code.authorize_url redirect_uri: redirect_uri, scope: 'view_issues view_project'
82
      uri = URI.parse url
83
      visit uri.path+'?'+uri.query
84

  
85
      find 'h2', visible: true, text: 'Authorization required'
86
      find 'p', visible: true, text: /Authorize Oauth Test/
87
      find '.oauth-permissions', visible: true, text: /View Issues/
88
      find '.oauth-permissions', visible: true, text: /View project/
89

  
90
      click_button 'Authorize'
91

  
92
      assert grant = app.access_grants.last
93
      assert_equal 'view_issues view_project', grant.scopes.to_s
94

  
95
      # check for output defined above in the request handler
96
      find 'p', visible: true, text: /Authorization succeeded/
97
      assert token.present?
98

  
99
      visit '/my/account'
100
      click_link 'Authorized applications'
101
      find 'td', visible: true, text: /Oauth Test/
102
      click_link 'Sign out'
103

  
104
      # Now, use the token for some API requests
105
      assert_raise(RestClient::Unauthorized) do
106
        RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json"
107
      end
108

  
109
      headers = { 'Authorization' => "Bearer #{token.token}" }
110
      r = RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json", headers
111
      issues = JSON.parse(r.body)['issues']
112
      assert issues.any?
113

  
114
      # time entries access is not part of the granted scopes
115
      assert_raise(RestClient::Forbidden) do
116
        RestClient.get "http://localhost:#{test_port}/projects/onlinestore/time_entries.json", headers
117
      end
118
    end
119
  end
120

  
121
  private
122

  
123
  def launch_client_app(port: 12345, path: '/')
124
    server = WEBrick::HTTPServer.new Port: port
125
    trap 'INT' do server.shutdown end
126
    server.mount_proc path do |req, res|
127
      yield req, res
128
    end
129
    Thread.new{ server.start }
130
    port
131
  end
132

  
133
  def test_port
134
    Capybara.current_session.server.port
135
  end
136
end
137

  
test/unit/user_test.rb
1327 1327
  else
1328 1328
    puts "Skipping openid tests."
1329 1329
  end
1330

  
1331
  def test_should_recognize_authorized_by_oauth
1332
    u = User.find 2
1333
    refute u.authorized_by_oauth?
1334
    u.oauth_scope = %i[add_issues view_issues]
1335
    assert u.authorized_by_oauth?
1336
  end
1337

  
1338
  def test_admin_should_be_limited_by_oauth_scope
1339
    u = User.find_by_admin(true)
1340
    assert u.admin?
1341

  
1342
    u.oauth_scope = %i[add_issues view_issues]
1343
    refute u.admin?
1344

  
1345
    u.oauth_scope = %i[add_issues view_issues admin]
1346
    assert u.admin?
1347

  
1348
    u = User.find_by_admin(false)
1349
    refute u.admin?
1350
    u.oauth_scope = %i[add_issues view_issues admin]
1351
    refute u.admin?
1352
  end
1353

  
1354
  def test_oauth_scope_should_limit_global_user_permissions
1355
    admin = User.find 1
1356
    user = User.find 2
1357
    [admin, user].each do |u|
1358
      assert u.allowed_to?(:add_issues, nil, global: true)
1359
      assert u.allowed_to?(:view_issues, nil, global: true)
1360
      u.oauth_scope = %i[view_issues]
1361
      refute u.allowed_to?(:add_issues, nil, global: true)
1362
      assert u.allowed_to?(:view_issues, nil, global: true)
1363
    end
1364
  end
1365

  
1366
  def test_oauth_scope_should_limit_project_user_permissions
1367
    admin = User.find 1
1368
    project = Project.find 5
1369
    assert admin.allowed_to?(:add_issues, project)
1370
    assert admin.allowed_to?(:view_issues, project)
1371
    admin.oauth_scope = %i[view_issues]
1372
    refute admin.allowed_to?(:add_issues, project)
1373
    assert admin.allowed_to?(:view_issues, project)
1374

  
1375
    admin.oauth_scope = %i[view_issues admin]
1376
    assert admin.allowed_to?(:add_issues, project)
1377
    assert admin.allowed_to?(:view_issues, project)
1378

  
1379
    user = User.find 2
1380
    project = Project.find 1
1381
    assert user.allowed_to?(:add_issues, project)
1382
    assert user.allowed_to?(:view_issues, project)
1383
    user.oauth_scope = %i[view_issues]
1384
    refute user.allowed_to?(:add_issues, project)
1385
    assert user.allowed_to?(:view_issues, project)
1386

  
1387
    user.oauth_scope = %i[view_issues admin]
1388
    refute user.allowed_to?(:add_issues, project)
1389
    assert user.allowed_to?(:view_issues, project)
1390
  end
1330 1391
end
(22-22/24)