Feature #24808 » 0004-Add-OAuth2-provider-capability-using-doorkeeper-gem.patch
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 |