0004-Add-OAuth2-provider-capability-using-doorkeeper-gem.patch

Jens Krämer, 2021-04-13 08:16

Download (42.2 KB)

View differences:

Gemfile
17 17
gem "rbpdf", "~> 1.20.0"
18 18
gem 'addressable'
19 19
gem 'rubyzip', '~> 2.3.0'
20
gem "doorkeeper", "~> 5.5.1"
21
gem "bcrypt", require: false
22
gem "doorkeeper-i18n", "~> 5.0"
20 23

  
21 24
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
22 25
gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin]
......
95 98
  gem 'rubocop', '~> 1.12.0'
96 99
  gem 'rubocop-performance', '~> 1.10.1'
97 100
  gem 'rubocop-rails', '~> 2.9.0'
101
  # for testing oauth provider capabilities
102
  gem 'oauth2'
103
  gem 'rest-client'
98 104
end
99 105

  
100 106
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
app/controllers/application_controller.rb
126 126
      if (key = api_key_from_request)
127 127
        # Use API key
128 128
        user = User.find_by_api_key(key)
129
      elsif access_token = Doorkeeper.authenticate(request)
130
        # Oauth
131
        if access_token.accessible?
132
          user = User.active.find_by_id(access_token.resource_owner_id)
133
          user.oauth_scope = access_token.scopes.all.map(&:to_sym)
134
        else
135
          doorkeeper_render_error
136
        end
129 137
      elsif /\ABasic /i.match?(request.authorization.to_s)
130 138
        # HTTP Basic, either username/password or API key/random
131 139
        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
101 101
  attr_accessor :password, :password_confirmation, :generate_password
102 102
  attr_accessor :last_before_login_on
103 103
  attr_accessor :remote_ip
104
  attr_writer   :oauth_scope
104 105

  
105 106
  LOGIN_LENGTH_LIMIT = 60
106 107
  MAIL_LENGTH_LIMIT = 60
......
720 721
    end
721 722
  end
722 723

  
724
  def admin?
725
    if authorized_by_oauth?
726
      # when signed in via oauth, the user only acts as admin when the admin scope is set
727
      super and @oauth_scope.include?(:admin)
728
    else
729
      super
730
    end
731
  end
732

  
733
  # true if the user has signed in via oauth
734
  def authorized_by_oauth?
735
    !@oauth_scope.nil?
736
  end
737

  
723 738
  # Return true if the user is allowed to do the specified action on a specific context
724 739
  # Action can be:
725 740
  # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
......
740 755

  
741 756
      roles.any? do |role|
742 757
        (context.is_public? || role.member?) &&
743
        role.allowed_to?(action) &&
758
        role.allowed_to?(action, @oauth_scope) &&
744 759
        (block_given? ? yield(role, self) : true)
745 760
      end
746 761
    elsif context && context.is_a?(Array)
......
759 774
      # authorize if user has at least one role that has this permission
760 775
      roles = self.roles.to_a | [builtin_role]
761 776
      roles.any? do |role|
762
        role.allowed_to?(action) &&
777
        role.allowed_to?(action, @oauth_scope) &&
763 778
        (block_given? ? yield(role, self) : true)
764 779
      end
765 780
    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
10 10
  api.last_login_on     @user.last_login_on
11 11
  api.passwd_changed_on @user.passwd_changed_on
12 12
  api.twofa_scheme      @user.twofa_scheme if User.current.admin? || (User.current == @user)
13
  api.api_key    @user.api_key if User.current.admin? || (User.current == @user)
13
  api.api_key    @user.api_key if (User.current.admin? || (User.current == @user && !User.current.authorized_by_oauth?))
14 14
  api.status     @user.status if User.current.admin?
15 15

  
16 16
  render_api_custom_values @user.visible_custom_field_values, api
config/application.rb
82 82
      :path => config.relative_url_root || '/'
83 83
    )
84 84

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

  
85 94
    if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
86 95
      instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
87 96
    end
config/initializers/doorkeeper.rb
1
# frozen_string_literal: true
2

  
3
Doorkeeper.configure do
4
  orm :active_record
5

  
6
  # Issue access tokens with refresh token
7
  use_refresh_token
8

  
9
  # Authorization Code expiration time (default: 10 minutes).
10
  #
11
  # authorization_code_expires_in 10.minutes
12

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

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

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

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

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

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

  
37
  allow_token_introspection false
38

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

  
42
  # Specify what redirect URI's you want to block during Application creation.
43
  forbid_redirect_uri { |uri| %w[data vbscript javascript].include?(uri.scheme.to_s.downcase) }
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
966 966
  permission_view_time_entries: Gebuchte Aufwände ansehen
967 967
  permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen
968 968
  permission_view_wiki_pages: Wiki ansehen
969
  permission_view_project: Projekte ansehen
970
  permission_search_project: Projekte suchen
971
  permission_view_members: Projektmitglieder anzeigen
969 972

  
970 973
  project_module_boards: Foren
971 974
  project_module_calendar: Kalender
......
1383 1386
  error_invalid_authenticity_token: Invalid form authenticity token.
1384 1387
  error_query_statement_invalid: An error occurred while executing the query and has
1385 1388
    been logged. Please report this error to your Redmine administrator.
1389
  label_oauth_permission_admin: Administrator-Zugriff
1390
  label_oauth_admin_access: Administration
1391
  label_oauth_application_plural: Applikationen
1392
  label_oauth_authorized_application_plural: Autorisierte Applikationen
1393
  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.
1394
  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.
1395
  text_oauth_copy_secret_now: Das Geheimnis bitte jetzt an einen sicheren Ort kopieren, es kann nicht erneut angezeigt werden.
1396
  text_oauth_implicit_permissions: Zugriff auf Benutzername, Login sowie auf die primäre Email-Adresse
1397
  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
579 579
  permission_manage_related_issues: Manage related issues
580 580
  permission_import_issues: Import issues
581 581
  permission_log_time_for_other_users: Log spent time for other users
582
  permission_view_project: View projects
583
  permission_search_project: Search projects
584
  permission_view_members: View project members
585

  
582 586

  
583 587
  project_module_issue_tracking: Issue tracking
584 588
  project_module_time_tracking: Time tracking
......
1356 1360

  
1357 1361
  text_user_destroy_confirmation: "Are you sure you want to delete this user and remove all references to them? This cannot be undone. Often, locking a user instead of deleting them is the better solution. To confirm, please enter their login (%{login}) below."
1358 1362
  text_project_destroy_enter_identifier: "To confirm, please enter the project's identifier (%{identifier}) below."
1363

  
1364
  label_oauth_permission_admin: Administrate this Redmine
1365
  label_oauth_admin_access: Administration
1366
  label_oauth_application_plural: Applications
1367
  label_oauth_authorized_application_plural: Authorized applications
1368
  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.
1369
  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.
1370
  text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown again.
1371
  text_oauth_implicit_permissions: View your name, login and primary email address
1372
  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
  use_doorkeeper do
22
    controllers :applications => 'oauth2_applications'
23
  end
24

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

  
23 28
  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
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
311 311
  menu.push :ldap_authentication,
312 312
            {:controller => 'auth_sources', :action => 'index'},
313 313
            :html => {:class => 'icon icon-server-authentication'}
314
  menu.push :applications, {:controller => 'oauth2_applications', :action => 'index'},
315
            :if => Proc.new { Setting.rest_api_enabled? },
316
            :caption => :'doorkeeper.layouts.admin.nav.applications',
317
            :html => {:class => 'icon icon-applications'}
314 318
  menu.push :plugins, {:controller => 'admin', :action => 'plugins'},
315 319
            :last => true,
316 320
            :html => {:class => 'icon icon-plugins'}
public/stylesheets/application.css
1042 1042
  color: #A6750C;
1043 1043
}
1044 1044

  
1045
.warning .oauth-permissions { display:inline-block;text-align:left; }
1046
.warning .oauth-permissions p { margin-top:0;-webkit-margin-before:0;}
1047

  
1045 1048
#errorExplanation ul { font-size: 0.9em;}
1046 1049
#errorExplanation h2, #errorExplanation p { display: none; }
1047 1050

  
......
1569 1572
.icon-workflows { background-image: url(../images/ticket_go.png); }
1570 1573
.icon-custom-fields { background-image: url(../images/textfield.png); }
1571 1574
.icon-plugins { background-image: url(../images/plugin.png); }
1575
.icon-applications { background-image: url(../images/application_view_tile.png); }
1576
.icon-authorize { background-image: url(../images/application_key.png); }
1572 1577
.icon-news { background-image: url(../images/news.png); }
1573 1578
.icon-issue-closed { background-image: url(../images/ticket_checked.png); }
1574 1579
.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
class OauthProviderSystemTest < ApplicationSystemTestCase
8
  fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
9
           :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues,
10
           :enumerations, :custom_fields, :custom_values, :custom_fields_trackers,
11
           :watchers, :journals, :journal_details, :versions,
12
           :workflows
13

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

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

  
32
      find('#doorkeeper_application_scopes_view_issues').set(true)
33
      click_button 'Create'
34
    end
35

  
36
    assert app = Doorkeeper::Application.find_by_name('Oauth Test')
37

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

  
43
    # scrape the clear text secret from the page
44
    app_secret = all(:css, 'p code')[1].text
45

  
46
    click_link 'Sign out'
47

  
48
    #
49
    # regular user authorizes the application
50
    #
51
    client = OAuth2::Client.new(app.uid, app_secret, site: "http://127.0.0.1:#{test_port}/")
52

  
53
    # set up a dummy http listener to handle the redirect
54
    port = rand 10000..20000
55
    redirect_uri = "http://127.0.0.1:#{port}"
56
    # the request handler below will set this to the auth token
57
    token = nil
58

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

  
69
    log_user 'jsmith', 'jsmith'
70
    with_settings rest_api_enabled: 1 do
71
      visit '/my/account'
72
      click_link 'Authorized applications'
73
      find 'p.nodata', visible: true
74

  
75
      # an oauth client would send the user to this url to request permission
76
      url = client.auth_code.authorize_url redirect_uri: redirect_uri, scope: 'view_issues view_project'
77
      uri = URI.parse url
78
      visit uri.path+'?'+uri.query
79

  
80
      find 'h2', visible: true, text: 'Authorization required'
81
      find 'p', visible: true, text: /Authorize Oauth Test/
82
      find '.oauth-permissions', visible: true, text: /View Issues/
83
      find '.oauth-permissions', visible: true, text: /View project/
84

  
85
      click_button 'Authorize'
86

  
87
      assert grant = app.access_grants.last
88
      assert_equal 'view_issues view_project', grant.scopes.to_s
89

  
90
      # check for output defined above in the request handler
91
      find 'p', visible: true, text: /Authorization succeeded/
92
      assert token.present?
93

  
94
      visit '/my/account'
95
      click_link 'Authorized applications'
96
      find 'td', visible: true, text: /Oauth Test/
97
      click_link 'Sign out'
98

  
99
      # Now, use the token for some API requests
100
      assert_raise(RestClient::Unauthorized) do
101
        RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json"
102
      end
103

  
104
      headers = { 'Authorization' => "Bearer #{token.token}" }
105
      r = RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json", headers
106
      issues = JSON.parse(r.body)['issues']
107
      assert issues.any?
108

  
109
      # time entries access is not part of the granted scopes
110
      assert_raise(RestClient::Forbidden) do
111
        RestClient.get "http://localhost:#{test_port}/projects/onlinestore/time_entries.json", headers
112
      end
113
    end
114
  end
115

  
116
  private
117

  
118
  def launch_client_app(port: 12345, path: '/', &block)
119
    server = WEBrick::HTTPServer.new Port: port
120
    trap('INT'){ server.shutdown }
121
    server.mount_proc(path, block)
122
    Thread.new{ server.start }
123
    port
124
  end
125

  
126
  def test_port
127
    Capybara.current_session.server.port
128
  end
129
end
130

  
test/unit/user_test.rb
1345 1345
  else
1346 1346
    puts "Skipping openid tests."
1347 1347
  end
1348

  
1349
  def test_should_recognize_authorized_by_oauth
1350
    u = User.find 2
1351
    assert_not u.authorized_by_oauth?
1352
    u.oauth_scope = [:add_issues, :view_issues]
1353
    assert u.authorized_by_oauth?
1354
  end
1355

  
1356
  def test_admin_should_be_limited_by_oauth_scope
1357
    u = User.find_by_admin(true)
1358
    assert u.admin?
1359

  
1360
    u.oauth_scope = [:add_issues, :view_issues]
1361
    assert_not u.admin?
1362

  
1363
    u.oauth_scope = [:add_issues, :view_issues, :admin]
1364
    assert u.admin?
1365

  
1366
    u = User.find_by_admin(false)
1367
    assert_not u.admin?
1368
    u.oauth_scope = [:add_issues, :view_issues, :admin]
1369
    assert_not u.admin?
1370
  end
1371

  
1372
  def test_oauth_scope_should_limit_global_user_permissions
1373
    admin = User.find 1
1374
    user = User.find 2
1375
    [admin, user].each do |u|
1376
      assert u.allowed_to?(:add_issues, nil, global: true)
1377
      assert u.allowed_to?(:view_issues, nil, global: true)
1378
      u.oauth_scope = [:view_issues]
1379
      assert_not u.allowed_to?(:add_issues, nil, global: true)
1380
      assert u.allowed_to?(:view_issues, nil, global: true)
1381
    end
1382
  end
1383

  
1384
  def test_oauth_scope_should_limit_project_user_permissions
1385
    admin = User.find 1
1386
    project = Project.find 5
1387
    assert admin.allowed_to?(:add_issues, project)
1388
    assert admin.allowed_to?(:view_issues, project)
1389
    admin.oauth_scope = [:view_issues]
1390
    assert_not admin.allowed_to?(:add_issues, project)
1391
    assert admin.allowed_to?(:view_issues, project)
1392

  
1393
    admin.oauth_scope = [:view_issues, :admin]
1394
    assert admin.allowed_to?(:add_issues, project)
1395
    assert admin.allowed_to?(:view_issues, project)
1396

  
1397
    user = User.find 2
1398
    project = Project.find 1
1399
    assert user.allowed_to?(:add_issues, project)
1400
    assert user.allowed_to?(:view_issues, project)
1401
    user.oauth_scope = [:view_issues]
1402
    assert_not user.allowed_to?(:add_issues, project)
1403
    assert user.allowed_to?(:view_issues, project)
1404

  
1405
    user.oauth_scope = [:view_issues, :admin]
1406
    assert_not user.allowed_to?(:add_issues, project)
1407
    assert user.allowed_to?(:view_issues, project)
1408
  end
1348 1409
end
1349
-