mirror of
https://github.com/redmine/redmine.git
synced 2025-10-26 00:36:14 +02:00
Add OAuth2 provider capability using doorkeeper gem (#24808).
Patch by Jens Krämer (user:jkraemer). git-svn-id: https://svn.redmine.org/redmine/trunk@23837 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
7
Gemfile
7
Gemfile
@@ -19,6 +19,9 @@ gem 'rack', '>= 3.1.3'
|
||||
gem "stimulus-rails", "~> 1.3"
|
||||
gem "importmap-rails", "~> 2.0"
|
||||
gem 'commonmarker', '~> 2.3.0'
|
||||
gem "doorkeeper", "~> 5.8.2"
|
||||
gem "bcrypt", require: false
|
||||
gem "doorkeeper-i18n", "~> 5.2"
|
||||
|
||||
# Ruby Standard Gems
|
||||
gem 'csv', '~> 3.3.2'
|
||||
@@ -115,6 +118,10 @@ group :test do
|
||||
gem 'rubocop-performance', '~> 1.25.0', require: false
|
||||
gem 'rubocop-rails', '~> 2.32.0', require: false
|
||||
gem 'bundle-audit', require: false
|
||||
# for testing oauth provider capabilities
|
||||
gem 'oauth2'
|
||||
gem 'rest-client'
|
||||
gem 'webrick'
|
||||
end
|
||||
|
||||
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
<path d="M12 15v6"/>
|
||||
<path d="M5 15h3l-3 6h3"/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--apps">
|
||||
<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M14 7l6 0"/>
|
||||
<path d="M17 4l0 6"/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--arrow-right">
|
||||
<path d="M4 9h8v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1z"/>
|
||||
</symbol>
|
||||
@@ -398,6 +405,10 @@
|
||||
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/>
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--shield-check">
|
||||
<path d="M11.46 20.846a12 12 0 0 1 -7.96 -14.846a12 12 0 0 0 8.5 -3a12 12 0 0 0 8.5 3a12 12 0 0 1 -.09 7.06"/>
|
||||
<path d="M15 19l2 2l4 -4"/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--stats">
|
||||
<path d="M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
@@ -1316,6 +1316,9 @@ div.flash.warning svg.icon-svg, .conflict svg.icon-svg {
|
||||
color: #A6750C;
|
||||
}
|
||||
|
||||
.warning .oauth-permissions { display:inline-block;text-align:left; }
|
||||
.warning .oauth-permissions p { margin-top:0;-webkit-margin-before:0;}
|
||||
|
||||
#errorExplanation ul { font-size: 0.9em;}
|
||||
#errorExplanation h2, #errorExplanation p { display: none; }
|
||||
|
||||
|
||||
@@ -131,6 +131,14 @@ class ApplicationController < ActionController::Base
|
||||
if (key = api_key_from_request)
|
||||
# Use API key
|
||||
user = User.find_by_api_key(key)
|
||||
elsif access_token = Doorkeeper.authenticate(request)
|
||||
# Oauth
|
||||
if access_token.accessible?
|
||||
user = User.active.find_by_id(access_token.resource_owner_id)
|
||||
user.oauth_scope = access_token.scopes.all.map(&:to_sym)
|
||||
else
|
||||
doorkeeper_render_error
|
||||
end
|
||||
elsif /\ABasic /i.match?(request.authorization.to_s)
|
||||
# HTTP Basic, either username/password or API key/random
|
||||
authenticate_with_http_basic do |username, password|
|
||||
|
||||
38
app/controllers/oauth2_applications_controller.rb
Normal file
38
app/controllers/oauth2_applications_controller.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006- Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
class Oauth2ApplicationsController < Doorkeeper::ApplicationsController
|
||||
private
|
||||
|
||||
def application_params
|
||||
params[:doorkeeper_application] ||= {}
|
||||
params[:doorkeeper_application][:scopes] ||= []
|
||||
|
||||
scopes = Redmine::AccessControl.public_permissions.map{|p| p.name.to_s}
|
||||
|
||||
if params[:doorkeeper_application][:scopes].is_a?(Array)
|
||||
scopes |= params[:doorkeeper_application][:scopes]
|
||||
else
|
||||
scopes |= params[:doorkeeper_application][:scopes].split(/\s+/)
|
||||
end
|
||||
params[:doorkeeper_application][:scopes] = scopes.join(' ')
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -198,11 +198,14 @@ class Role < ApplicationRecord
|
||||
# action can be:
|
||||
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
||||
# * a permission Symbol (eg. :edit_project)
|
||||
def allowed_to?(action)
|
||||
# scope can be:
|
||||
# * an array of permissions which will be used as filter (logical AND)
|
||||
|
||||
def allowed_to?(action, scope=nil)
|
||||
if action.is_a? Hash
|
||||
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
|
||||
allowed_actions(scope).include? "#{action[:controller]}/#{action[:action]}"
|
||||
else
|
||||
allowed_permissions.include? action
|
||||
allowed_permissions(scope).include? action
|
||||
end
|
||||
end
|
||||
|
||||
@@ -298,13 +301,20 @@ class Role < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def allowed_permissions
|
||||
@allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
|
||||
def allowed_permissions(scope = nil)
|
||||
scope = scope.sort if scope.present? # to maintain stable cache keys
|
||||
@allowed_permissions ||= {}
|
||||
@allowed_permissions[scope] ||= begin
|
||||
unscoped = permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
|
||||
scope.present? ? unscoped & scope : unscoped
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_actions
|
||||
@actions_allowed ||=
|
||||
allowed_permissions.inject([]) do |actions, permission|
|
||||
def allowed_actions(scope = nil)
|
||||
scope = scope.sort if scope.present? # to maintain stable cache keys
|
||||
@actions_allowed ||= {}
|
||||
@actions_allowed[scope] ||=
|
||||
allowed_permissions(scope).inject([]) do |actions, permission|
|
||||
actions += Redmine::AccessControl.allowed_actions(permission)
|
||||
end.flatten
|
||||
end
|
||||
|
||||
@@ -112,6 +112,7 @@ class User < Principal
|
||||
attr_accessor :password, :password_confirmation, :generate_password
|
||||
attr_accessor :last_before_login_on
|
||||
attr_accessor :remote_ip
|
||||
attr_writer :oauth_scope
|
||||
|
||||
LOGIN_LENGTH_LIMIT = 60
|
||||
MAIL_LENGTH_LIMIT = 254
|
||||
@@ -732,6 +733,20 @@ class User < Principal
|
||||
end
|
||||
end
|
||||
|
||||
def admin?
|
||||
if authorized_by_oauth?
|
||||
# when signed in via oauth, the user only acts as admin when the admin scope is set
|
||||
super and @oauth_scope.include?(:admin)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# true if the user has signed in via oauth
|
||||
def authorized_by_oauth?
|
||||
!@oauth_scope.nil?
|
||||
end
|
||||
|
||||
# Return true if the user is allowed to do the specified action on a specific context
|
||||
# Action can be:
|
||||
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
||||
@@ -752,7 +767,7 @@ class User < Principal
|
||||
|
||||
roles.any? do |role|
|
||||
(context.is_public? || role.member?) &&
|
||||
role.allowed_to?(action) &&
|
||||
role.allowed_to?(action, @oauth_scope) &&
|
||||
(block ? yield(role, self) : true)
|
||||
end
|
||||
elsif context && context.is_a?(Array)
|
||||
@@ -771,7 +786,7 @@ class User < Principal
|
||||
# authorize if user has at least one role that has this permission
|
||||
roles = self.roles.to_a | [builtin_role]
|
||||
roles.any? do |role|
|
||||
role.allowed_to?(action) &&
|
||||
role.allowed_to?(action, @oauth_scope) &&
|
||||
(block ? yield(role, self) : true)
|
||||
end
|
||||
else
|
||||
|
||||
39
app/views/doorkeeper/applications/_form.html.erb
Normal file
39
app/views/doorkeeper/applications/_form.html.erb
Normal file
@@ -0,0 +1,39 @@
|
||||
<%= error_messages_for 'application' %>
|
||||
<div class="box tabular">
|
||||
<p><%= f.text_field :name, :required => true %></p>
|
||||
|
||||
<p>
|
||||
<%= f.text_area :redirect_uri, :required => true, :size => 60, :label => :'activerecord.attributes.doorkeeper/application.redirect_uri' %>
|
||||
<em class="info">
|
||||
<%= t('doorkeeper.applications.help.redirect_uri') %>
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3><%= l(:'activerecord.attributes.doorkeeper/application.scopes') %></h3>
|
||||
<p><em class="info"><%= l :text_oauth_info_scopes %></em></p>
|
||||
<div class="box tabular" id="scopes">
|
||||
<fieldset><legend><%= l :label_oauth_admin_access %></legend>
|
||||
<label class="floating" style="width: auto;">
|
||||
<%= check_box_tag 'doorkeeper_application[scopes][]', 'admin', @application.scopes.include?('admin'),
|
||||
:id => "doorkeeper_application_scopes_admin"
|
||||
%>
|
||||
<%= l :text_oauth_admin_permission %>
|
||||
</label>
|
||||
</fieldset>
|
||||
<% perms_by_module = Redmine::AccessControl.permissions.group_by {|p| p.project_module.to_s} %>
|
||||
<% perms_by_module.keys.sort.each do |mod| %>
|
||||
<fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
|
||||
<% perms_by_module[mod].each do |permission| %>
|
||||
<label class="floating">
|
||||
<%= check_box_tag 'doorkeeper_application[scopes][]', permission.name.to_s, (permission.public? || @application.scopes.include?( permission.name.to_s)),
|
||||
:id => "doorkeeper_application_scopes_#{permission.name}",
|
||||
:disabled => permission.public? %>
|
||||
<%= l_or_humanize(permission.name, :prefix => 'permission_') %>
|
||||
</label>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
<br /><%= check_all_links 'scopes' %>
|
||||
<%= hidden_field_tag 'doorkeeper_application[scopes][]', '' %>
|
||||
</div>
|
||||
6
app/views/doorkeeper/applications/edit.html.erb
Normal file
6
app/views/doorkeeper/applications/edit.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
|
||||
|
||||
<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %>
|
||||
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<% end %>
|
||||
33
app/views/doorkeeper/applications/index.html.erb
Normal file
33
app/views/doorkeeper/applications/index.html.erb
Normal file
@@ -0,0 +1,33 @@
|
||||
<div class="contextual">
|
||||
<%= link_to sprite_icon('add', t('.new')), new_oauth_application_path, :class => 'icon icon-add' %>
|
||||
</div>
|
||||
|
||||
<%= title l 'label_oauth_application_plural' %>
|
||||
|
||||
<% if @applications.any? %>
|
||||
<div class="autoscroll">
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= t('.name') %></th>
|
||||
<th><%= t('.callback_url') %></th>
|
||||
<th><%= t('.scopes') %></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% @applications.each do |application| %>
|
||||
<tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
|
||||
<td class="name"><span><%= link_to application.name, oauth_application_path(application) %></span></td>
|
||||
<td class="description"><%= truncate application.redirect_uri.split.join(', '), length: 50 %></td>
|
||||
<td class="description"><%= safe_join application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(application), class: 'icon icon-edit' %>
|
||||
<%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
6
app/views/doorkeeper/applications/new.html.erb
Normal file
6
app/views/doorkeeper/applications/new.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<%= title [l('label_oauth_application_plural'), oauth_applications_path], t('.title') %>
|
||||
|
||||
<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %>
|
||||
<%= render :partial => 'form', :locals => { :f => f } %>
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<% end %>
|
||||
54
app/views/doorkeeper/applications/show.html.erb
Normal file
54
app/views/doorkeeper/applications/show.html.erb
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="contextual">
|
||||
<%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(@application), :accesskey => accesskey(:edit), class: 'icon icon-edit' %>
|
||||
<%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(@application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
|
||||
</div>
|
||||
|
||||
<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
|
||||
|
||||
<div class="box">
|
||||
<h3 class="icon icon-passwd"><%= sprite_icon('key', l(:label_information_plural)) %></h3>
|
||||
<p>
|
||||
<span class="label"><%= t('.application_id') %>:</span>
|
||||
<code><%= h @application.uid %></code>
|
||||
</p>
|
||||
<p>
|
||||
<span class="label"><%= t('.secret') %>:</span>
|
||||
<code>
|
||||
<% secret = flash[:application_secret].presence || @application.plaintext_secret %>
|
||||
<% flash.delete :application_secret %>
|
||||
<% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
|
||||
<%= t('.secret_hashed') %>
|
||||
<% else %>
|
||||
<%= secret %>
|
||||
<% end %>
|
||||
</code>
|
||||
<% if secret.present? && Doorkeeper.config.application_secret_hashed? %>
|
||||
<strong><%= t "text_oauth_copy_secret_now" %></strong>
|
||||
<% end %>
|
||||
</p>
|
||||
<p>
|
||||
<span class="label"><%= t('.scopes') %>:</span>
|
||||
<code><%= safe_join @application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3><%= t('.callback_urls') %></h3>
|
||||
|
||||
<div class="autoscroll">
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= t('.callback_url') %></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% @application.redirect_uri.split.each do |uri| %>
|
||||
<tr class="<%= cycle("odd", "even") %>">
|
||||
<td class="name"><span><%= uri %></span></td>
|
||||
<td class="buttons">
|
||||
<%= link_to sprite_icon('shield-check', 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' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
6
app/views/doorkeeper/authorizations/error.html.erb
Normal file
6
app/views/doorkeeper/authorizations/error.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<h2><%= t('doorkeeper.authorizations.error.title') %></h2>
|
||||
|
||||
<p id="errorExplanation"><%= @pre_auth.error_response.body[:error_description] %></p>
|
||||
<p><a href="javascript:history.back()"><%= l(:button_back) %></a></p>
|
||||
|
||||
<% html_title t('doorkeeper.authorizations.error.title') %>
|
||||
48
app/views/doorkeeper/authorizations/new.html.erb
Normal file
48
app/views/doorkeeper/authorizations/new.html.erb
Normal file
@@ -0,0 +1,48 @@
|
||||
<%= title t('.title') %>
|
||||
|
||||
<div class="warning">
|
||||
<p><strong><%=h @pre_auth.client.name %></strong></p>
|
||||
|
||||
<p><%= raw t('.prompt', client_name: content_tag(:strong, class: "text-info") { @pre_auth.client.name }) %></p>
|
||||
|
||||
<div class="oauth-permissions">
|
||||
<p><%= t('.able_to') %>:</p>
|
||||
<ul>
|
||||
<li><%= l :text_oauth_implicit_permissions %></li>
|
||||
<% @pre_auth.scopes.each do |scope| %>
|
||||
<% if scope == 'admin' %>
|
||||
<li><%= l :label_oauth_permission_admin %></li>
|
||||
<% else %>
|
||||
<li><%= l_or_humanize(scope, prefix: 'permission_') %></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% if @pre_auth.scopes.include?('admin') %>
|
||||
<p><%= l :text_oauth_admin_permission_info %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= form_tag oauth_authorization_path, method: :post do %>
|
||||
<%= hidden_field_tag :client_id, @pre_auth.client.uid %>
|
||||
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
|
||||
<%= hidden_field_tag :state, @pre_auth.state %>
|
||||
<%= hidden_field_tag :response_type, @pre_auth.response_type %>
|
||||
<%= hidden_field_tag :scope, @pre_auth.scope %>
|
||||
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
|
||||
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
|
||||
<%= submit_tag t('doorkeeper.authorizations.buttons.authorize') %>
|
||||
<% end %>
|
||||
<%= form_tag oauth_authorization_path, method: :delete do %>
|
||||
<%= hidden_field_tag :client_id, @pre_auth.client.uid %>
|
||||
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
|
||||
<%= hidden_field_tag :state, @pre_auth.state %>
|
||||
<%= hidden_field_tag :response_type, @pre_auth.response_type %>
|
||||
<%= hidden_field_tag :scope, @pre_auth.scope %>
|
||||
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
|
||||
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
|
||||
<%= submit_tag t('doorkeeper.authorizations.buttons.deny') %>
|
||||
<% end %>
|
||||
</p>
|
||||
8
app/views/doorkeeper/authorizations/show.html.erb
Normal file
8
app/views/doorkeeper/authorizations/show.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<%= title [l('label_oauth_authorized_application_plural'), oauth_authorized_applications_path] %>
|
||||
|
||||
<fieldset class="tabular"><legend><%= l(:label_information_plural) %></legend>
|
||||
<p>
|
||||
<label><%= t('.title') %>:</label>
|
||||
<code><%= params[:code] %></code>
|
||||
</p>
|
||||
</fieldset>
|
||||
31
app/views/doorkeeper/authorized_applications/index.html.erb
Normal file
31
app/views/doorkeeper/authorized_applications/index.html.erb
Normal file
@@ -0,0 +1,31 @@
|
||||
<%= title [t(:label_my_account), my_account_path], l('label_oauth_authorized_application_plural') %>
|
||||
|
||||
<% if @applications.any? %>
|
||||
<div class="autoscroll">
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= t('doorkeeper.authorized_applications.index.application') %></th>
|
||||
<th><%= t('doorkeeper.authorized_applications.index.created_at') %></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% @applications.each do |application| %>
|
||||
<tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
|
||||
<td class="name"><span><%= application.name %></span></td>
|
||||
<td ><%= format_date application.created_at %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to sprite_icon('del', 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' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<% @user = User.current %>
|
||||
<%= render :partial => 'my/sidebar' %>
|
||||
<% end %>
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="contextual">
|
||||
<%= additional_emails_link(@user) %>
|
||||
<%= link_to(sprite_icon('key', l(:button_change_password)), { :action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
|
||||
<%= link_to(sprite_icon('apps', l('label_oauth_authorized_application_plural')), oauth_authorized_applications_path, :class => 'icon icon-applications') if Setting.rest_api_enabled? %>
|
||||
<%= call_hook(:view_my_account_contextual, :user => @user)%>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ api.user do
|
||||
api.passwd_changed_on @user.passwd_changed_on
|
||||
api.avatar_url gravatar_url(@user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if @user.mail && Setting.gravatar_enabled?
|
||||
api.twofa_scheme @user.twofa_scheme if User.current.admin? || (User.current == @user)
|
||||
api.api_key @user.api_key if User.current.admin? || (User.current == @user)
|
||||
api.api_key @user.api_key if (User.current.admin? || (User.current == @user && !User.current.authorized_by_oauth?))
|
||||
api.status @user.status if User.current.admin?
|
||||
|
||||
render_api_custom_values @user.visible_custom_field_values, api
|
||||
|
||||
@@ -10,6 +10,72 @@ Rails.application.config.to_prepare do
|
||||
ActiveSupport::XmlMini.backend = 'Nokogiri'
|
||||
|
||||
Redmine::Preparation.prepare
|
||||
|
||||
Doorkeeper.configure do
|
||||
orm :active_record
|
||||
|
||||
# Issue access tokens with refresh token
|
||||
use_refresh_token
|
||||
|
||||
# Authorization Code expiration time (default: 10 minutes).
|
||||
#
|
||||
# authorization_code_expires_in 10.minutes
|
||||
|
||||
# Access token expiration time (default: 2 hours).
|
||||
# If you want to disable expiration, set this to `nil`.
|
||||
#
|
||||
# access_token_expires_in 2.hours
|
||||
|
||||
# Hash access and refresh tokens before persisting them.
|
||||
# https://doorkeeper.gitbook.io/guides/security/token-and-application-secrets
|
||||
hash_token_secrets
|
||||
|
||||
# Hash application secrets before persisting them.
|
||||
hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt'
|
||||
|
||||
# limit supported flows to Auth code
|
||||
grant_flows ['authorization_code']
|
||||
|
||||
realm Redmine::Info.app_name
|
||||
base_controller 'ApplicationController'
|
||||
default_scopes(*Redmine::AccessControl.public_permissions.map(&:name))
|
||||
optional_scopes(*(Redmine::AccessControl.permissions.map(&:name) << :admin))
|
||||
|
||||
# Forbids creating/updating applications with arbitrary scopes that are
|
||||
# not in configuration, i.e. +default_scopes+ or +optional_scopes+.
|
||||
enforce_configured_scopes
|
||||
|
||||
allow_token_introspection false
|
||||
|
||||
# allow http loopback redirect URIs but require https for all others
|
||||
force_ssl_in_redirect_uri { |uri| !%w[localhost 127.0.0.1 web localohst:8080].include?(uri.host) }
|
||||
|
||||
# Specify what redirect URI's you want to block during Application creation.
|
||||
forbid_redirect_uri { |uri| %w[data vbscript javascript].include?(uri.scheme.to_s.downcase) }
|
||||
|
||||
resource_owner_authenticator do
|
||||
if require_login
|
||||
if Setting.rest_api_enabled?
|
||||
User.current
|
||||
else
|
||||
deny_access
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
admin_authenticator do |_routes|
|
||||
if !Setting.rest_api_enabled? || !User.current.admin?
|
||||
deny_access
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Use Redmine standard layouts and helpers for Doorkeeper OAuth2 screens
|
||||
Doorkeeper::ApplicationsController.layout "admin"
|
||||
Doorkeeper::ApplicationsController.main_menu = false
|
||||
Doorkeeper::AuthorizationsController.layout "base"
|
||||
Doorkeeper::AuthorizedApplicationsController.layout "base"
|
||||
Doorkeeper::AuthorizedApplicationsController.main_menu = false
|
||||
end
|
||||
|
||||
# Load the secret token from the Redmine configuration file
|
||||
@@ -40,6 +106,14 @@ Rails.application.config.to_prepare do
|
||||
paths = theme.asset_paths
|
||||
Rails.application.config.assets.redmine_extension_paths << paths if paths.present?
|
||||
end
|
||||
|
||||
Doorkeeper::ApplicationsController.class_eval do
|
||||
require_sudo_mode :create, :show, :update, :destroy
|
||||
end
|
||||
|
||||
Doorkeeper::AuthorizationsController.class_eval do
|
||||
require_sudo_mode :create, :destroy
|
||||
end
|
||||
end
|
||||
|
||||
Rails.application.deprecators[:redmine] = ActiveSupport::Deprecation.new('7.0', 'Redmine')
|
||||
|
||||
9
config/initializers/doorkeeper.rb
Normal file
9
config/initializers/doorkeeper.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Lint/EmptyBlock
|
||||
Doorkeeper.configure do
|
||||
end
|
||||
|
||||
Rails.application.config.to_prepare do
|
||||
end
|
||||
# rubocop:enable Lint/EmptyBlock
|
||||
@@ -971,6 +971,9 @@ de:
|
||||
permission_view_time_entries: Gebuchte Aufwände ansehen
|
||||
permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen
|
||||
permission_view_wiki_pages: Wiki ansehen
|
||||
permission_view_project: Projekte ansehen
|
||||
permission_search_project: Projekte suchen
|
||||
permission_view_members: Projektmitglieder anzeigen
|
||||
|
||||
project_module_boards: Foren
|
||||
project_module_calendar: Kalender
|
||||
@@ -1477,3 +1480,12 @@ de:
|
||||
other: "%{count} others"
|
||||
text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
|
||||
to generate their avatars.
|
||||
label_oauth_permission_admin: Administrator-Zugriff
|
||||
label_oauth_admin_access: Administration
|
||||
label_oauth_application_plural: Applikationen
|
||||
label_oauth_authorized_application_plural: Autorisierte Applikationen
|
||||
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.
|
||||
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.
|
||||
text_oauth_copy_secret_now: Das Geheimnis bitte jetzt an einen sicheren Ort kopieren, es kann nicht erneut angezeigt werden.
|
||||
text_oauth_implicit_permissions: Zugriff auf Benutzername, Login sowie auf die primäre Email-Adresse
|
||||
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.
|
||||
@@ -139,6 +139,9 @@ en:
|
||||
must_contain_special_chars: "must contain special characters (!, $, %, ...)"
|
||||
domain_not_allowed: "contains a domain not allowed (%{domain})"
|
||||
too_simple: "is too simple"
|
||||
attributes:
|
||||
doorkeeper/application:
|
||||
scopes: Scopes
|
||||
|
||||
actionview_instancetag_blank_option: Please select
|
||||
|
||||
@@ -605,6 +608,10 @@ en:
|
||||
permission_manage_related_issues: Manage related issues
|
||||
permission_import_issues: Import issues
|
||||
permission_log_time_for_other_users: Log spent time for other users
|
||||
permission_view_project: View projects
|
||||
permission_search_project: Search projects
|
||||
permission_view_members: View project members
|
||||
|
||||
|
||||
project_module_issue_tracking: Issue tracking
|
||||
project_module_time_tracking: Time tracking
|
||||
@@ -1158,6 +1165,10 @@ en:
|
||||
label_time_by_author: "%{time} by %{author}"
|
||||
label_involved_principals: Author / Previous assignee
|
||||
label_progressbar: Progress bar
|
||||
label_oauth_permission_admin: Administrate this Redmine
|
||||
label_oauth_admin_access: Administration
|
||||
label_oauth_application_plural: Applications
|
||||
label_oauth_authorized_application_plural: Authorized applications
|
||||
|
||||
button_login: Login
|
||||
button_submit: Submit
|
||||
@@ -1343,6 +1354,11 @@ en:
|
||||
text_allowed_queries_to_select: Public (to any users) queries only selectable
|
||||
text_setting_config_change: You can configure the behaviour in config/configuration.yml. Please restart the application after editing it.
|
||||
text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> to generate their avatars.
|
||||
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.
|
||||
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.
|
||||
text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown again.
|
||||
text_oauth_implicit_permissions: View your name, login and primary email address
|
||||
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.
|
||||
|
||||
default_role_manager: Manager
|
||||
default_role_developer: Developer
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
Rails.application.routes.draw do
|
||||
use_doorkeeper do
|
||||
controllers :applications => 'oauth2_applications'
|
||||
end
|
||||
|
||||
root :to => 'welcome#index'
|
||||
root :to => 'welcome#index', :as => 'home'
|
||||
|
||||
match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
|
||||
|
||||
68
db/migrate/20250611092155_create_doorkeeper_tables.rb
Normal file
68
db/migrate/20250611092155_create_doorkeeper_tables.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
class CreateDoorkeeperTables < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :oauth_applications do |t|
|
||||
t.string :name, null: false
|
||||
t.string :uid, null: false
|
||||
t.string :secret, null: false
|
||||
t.text :redirect_uri, null: false
|
||||
t.text :scopes, null: false
|
||||
t.boolean :confidential, null: false, default: true
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
add_index :oauth_applications, :uid, unique: true
|
||||
|
||||
create_table :oauth_access_grants do |t|
|
||||
t.integer :resource_owner_id, null: false
|
||||
t.references :application, null: false
|
||||
t.string :token, null: false
|
||||
t.integer :expires_in, null: false
|
||||
t.text :redirect_uri, null: false
|
||||
t.datetime :created_at, null: false
|
||||
t.datetime :revoked_at
|
||||
t.text :scopes
|
||||
end
|
||||
|
||||
add_index :oauth_access_grants, :token, unique: true
|
||||
add_foreign_key(
|
||||
:oauth_access_grants,
|
||||
:oauth_applications,
|
||||
column: :application_id
|
||||
)
|
||||
add_foreign_key(
|
||||
:oauth_access_grants,
|
||||
:users,
|
||||
column: :resource_owner_id
|
||||
)
|
||||
|
||||
create_table :oauth_access_tokens do |t|
|
||||
t.integer :resource_owner_id
|
||||
t.references :application
|
||||
|
||||
t.string :token, null: false
|
||||
|
||||
t.string :refresh_token
|
||||
t.integer :expires_in
|
||||
t.datetime :revoked_at
|
||||
t.datetime :created_at, null: false
|
||||
t.text :scopes
|
||||
|
||||
t.string :previous_refresh_token, null: false, default: ""
|
||||
end
|
||||
|
||||
add_index :oauth_access_tokens, :token, unique: true
|
||||
add_index :oauth_access_tokens, :resource_owner_id
|
||||
add_index :oauth_access_tokens, :refresh_token, unique: true
|
||||
|
||||
add_foreign_key(
|
||||
:oauth_access_tokens,
|
||||
:oauth_applications,
|
||||
column: :application_id
|
||||
)
|
||||
add_foreign_key(
|
||||
:oauth_access_tokens,
|
||||
:users,
|
||||
column: :resource_owner_id
|
||||
)
|
||||
end
|
||||
end
|
||||
8
db/migrate/20250611092227_enable_pkce.rb
Normal file
8
db/migrate/20250611092227_enable_pkce.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EnablePkce < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :oauth_access_grants, :code_challenge, :string, null: true
|
||||
add_column :oauth_access_grants, :code_challenge_method, :string, null: true
|
||||
end
|
||||
end
|
||||
@@ -280,6 +280,11 @@ module Redmine
|
||||
{:controller => 'auth_sources', :action => 'index'},
|
||||
:icon => 'server-authentication',
|
||||
:html => {:class => 'icon icon-server-authentication'}
|
||||
menu.push :applications, {:controller => 'oauth2_applications', :action => 'index'},
|
||||
:if => Proc.new { Setting.rest_api_enabled? },
|
||||
:caption => :'doorkeeper.layouts.admin.nav.applications',
|
||||
:icon => 'apps',
|
||||
:html => {:class => 'icon icon-applications'}
|
||||
menu.push :plugins, {:controller => 'admin', :action => 'plugins'},
|
||||
:last => true,
|
||||
:icon => 'plugins',
|
||||
|
||||
131
test/system/oauth_provider_test.rb
Normal file
131
test/system/oauth_provider_test.rb
Normal file
@@ -0,0 +1,131 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../application_system_test_case'
|
||||
require 'oauth2'
|
||||
require 'webrick'
|
||||
|
||||
class OauthProviderSystemTest < ApplicationSystemTestCase
|
||||
fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
|
||||
:trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues,
|
||||
:enumerations, :custom_fields, :custom_values, :custom_fields_trackers,
|
||||
:watchers, :journals, :journal_details, :versions,
|
||||
:workflows
|
||||
|
||||
test 'application creation and authorization' do
|
||||
#
|
||||
# admin creates the application, granting permissions and generating a uuid
|
||||
# and secret.
|
||||
#
|
||||
log_user 'admin', 'admin'
|
||||
with_settings rest_api_enabled: 1 do
|
||||
visit '/admin'
|
||||
within 'div#admin-menu ul' do
|
||||
click_link 'Applications'
|
||||
end
|
||||
click_link 'New Application'
|
||||
fill_in 'Name', with: 'Oauth Test'
|
||||
|
||||
# as per https://tools.ietf.org/html/rfc8252#section-7.3, the port can be
|
||||
# anything when the redirect URI's host is 127.0.0.1.
|
||||
fill_in 'Redirect URI', with: 'http://127.0.0.1'
|
||||
|
||||
check 'View Issues'
|
||||
click_button 'Create'
|
||||
end
|
||||
|
||||
assert_text "Application created"
|
||||
|
||||
assert app = Doorkeeper::Application.find_by_name('Oauth Test')
|
||||
|
||||
find 'h2', visible: true, text: /Oauth Test/
|
||||
find 'p code', visible: true, text: app.uid
|
||||
find 'p strong', visible: true, text: /will not be shown again/
|
||||
find 'p code', visible: true, text: /View Issues/
|
||||
|
||||
# scrape the clear text secret from the page
|
||||
app_secret = all(:css, 'p code')[1].text
|
||||
|
||||
click_link 'Sign out'
|
||||
|
||||
#
|
||||
# regular user authorizes the application
|
||||
#
|
||||
client = OAuth2::Client.new(app.uid, app_secret, site: "http://127.0.0.1:#{test_port}/")
|
||||
|
||||
# set up a dummy http listener to handle the redirect
|
||||
port = rand 10000..20000
|
||||
redirect_uri = "http://127.0.0.1:#{port}"
|
||||
# the request handler below will set this to the auth token
|
||||
token = nil
|
||||
|
||||
# launches webrick, listening for the redirect with the auth code.
|
||||
launch_client_app(port: port) do |req, res|
|
||||
# get access code from code url param
|
||||
if code = req.query['code'].presence
|
||||
# exchange it for token
|
||||
token = client.auth_code.get_token(code, redirect_uri: redirect_uri)
|
||||
res.body = "<html><body><p>Authorization succeeded, you may close this window now.</p></body></html>"
|
||||
end
|
||||
end
|
||||
|
||||
log_user 'jsmith', 'jsmith'
|
||||
with_settings rest_api_enabled: 1 do
|
||||
visit '/my/account'
|
||||
click_link 'Authorized applications'
|
||||
find 'p.nodata', visible: true
|
||||
|
||||
# an oauth client would send the user to this url to request permission
|
||||
url = client.auth_code.authorize_url redirect_uri: redirect_uri, scope: 'view_issues view_project'
|
||||
uri = URI.parse url
|
||||
visit uri.path + '?' + uri.query
|
||||
|
||||
find 'h2', visible: true, text: 'Authorization required'
|
||||
find 'p', visible: true, text: /Authorize Oauth Test/
|
||||
find '.oauth-permissions', visible: true, text: /View Issues/
|
||||
find '.oauth-permissions', visible: true, text: /View project/
|
||||
|
||||
click_button 'Authorize'
|
||||
|
||||
assert grant = app.access_grants.last
|
||||
assert_equal 'view_issues view_project', grant.scopes.to_s
|
||||
|
||||
# check for output defined above in the request handler
|
||||
find 'p', visible: true, text: /Authorization succeeded/
|
||||
assert token.present?
|
||||
|
||||
visit '/my/account'
|
||||
click_link 'Authorized applications'
|
||||
find 'td', visible: true, text: /Oauth Test/
|
||||
click_link 'Sign out'
|
||||
|
||||
# Now, use the token for some API requests
|
||||
assert_raise(RestClient::Unauthorized) do
|
||||
RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json"
|
||||
end
|
||||
|
||||
headers = { 'Authorization' => "Bearer #{token.token}" }
|
||||
r = RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json", headers
|
||||
issues = JSON.parse(r.body)['issues']
|
||||
assert issues.any?
|
||||
|
||||
# time entries access is not part of the granted scopes
|
||||
assert_raise(RestClient::Forbidden) do
|
||||
RestClient.get "http://localhost:#{test_port}/projects/onlinestore/time_entries.json", headers
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def launch_client_app(port: 12345, path: '/', &block)
|
||||
server = WEBrick::HTTPServer.new Port: port
|
||||
trap('INT') { server.shutdown }
|
||||
server.mount_proc(path, block)
|
||||
Thread.new { server.start }
|
||||
port
|
||||
end
|
||||
|
||||
def test_port
|
||||
Capybara.current_session.server.port
|
||||
end
|
||||
end
|
||||
@@ -175,6 +175,32 @@ class RoleTest < ActiveSupport::TestCase
|
||||
assert_equal false, role.permissions_tracker_ids?(:view_issues, 1)
|
||||
end
|
||||
|
||||
def test_allowed_to_with_symbol
|
||||
role = Role.create!(:name => 'Test', :permissions => [:view_issues])
|
||||
assert_equal true, role.allowed_to?(:view_issues)
|
||||
assert_equal false, role.allowed_to?(:add_issues)
|
||||
end
|
||||
|
||||
def test_allowed_to_with_symbol_and_scope
|
||||
role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues])
|
||||
assert_equal true, role.allowed_to?(:view_issues, [:view_issues, :add_issues])
|
||||
assert_equal false, role.allowed_to?(:add_issues, [:view_issues, :add_issues])
|
||||
assert_equal false, role.allowed_to?(:delete_issues, [:view_issues, :add_issues])
|
||||
end
|
||||
|
||||
def test_allowed_to_with_hash
|
||||
role = Role.create!(:name => 'Test', :permissions => [:view_issues])
|
||||
assert_equal true, role.allowed_to?(:controller => 'issues', :action => 'show')
|
||||
assert_equal false, role.allowed_to?(:controller => 'issues', :action => 'create')
|
||||
end
|
||||
|
||||
def test_allowed_to_with_hash_and_scope
|
||||
role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues])
|
||||
assert_equal true, role.allowed_to?({:controller => 'issues', :action => 'show'}, [:view_issues, :add_issues])
|
||||
assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'create'}, [:view_issues, :add_issues])
|
||||
assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'destroy'}, [:view_issues, :add_issues])
|
||||
end
|
||||
|
||||
def test_has_permission_without_permissions
|
||||
role = Role.create!(:name => 'Test')
|
||||
assert_equal false, role.has_permission?(:delete_issues)
|
||||
|
||||
@@ -1398,6 +1398,67 @@ class UserTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_recognize_authorized_by_oauth
|
||||
u = User.find 2
|
||||
assert_not u.authorized_by_oauth?
|
||||
u.oauth_scope = [:add_issues, :view_issues]
|
||||
assert u.authorized_by_oauth?
|
||||
end
|
||||
|
||||
def test_admin_should_be_limited_by_oauth_scope
|
||||
u = User.find_by_admin(true)
|
||||
assert u.admin?
|
||||
|
||||
u.oauth_scope = [:add_issues, :view_issues]
|
||||
assert_not u.admin?
|
||||
|
||||
u.oauth_scope = [:add_issues, :view_issues, :admin]
|
||||
assert u.admin?
|
||||
|
||||
u = User.find_by_admin(false)
|
||||
assert_not u.admin?
|
||||
u.oauth_scope = [:add_issues, :view_issues, :admin]
|
||||
assert_not u.admin?
|
||||
end
|
||||
|
||||
def test_oauth_scope_should_limit_global_user_permissions
|
||||
admin = User.find 1
|
||||
user = User.find 2
|
||||
[admin, user].each do |u|
|
||||
assert u.allowed_to?(:add_issues, nil, global: true)
|
||||
assert u.allowed_to?(:view_issues, nil, global: true)
|
||||
u.oauth_scope = [:view_issues]
|
||||
assert_not u.allowed_to?(:add_issues, nil, global: true)
|
||||
assert u.allowed_to?(:view_issues, nil, global: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_oauth_scope_should_limit_project_user_permissions
|
||||
admin = User.find 1
|
||||
project = Project.find 5
|
||||
assert admin.allowed_to?(:add_issues, project)
|
||||
assert admin.allowed_to?(:view_issues, project)
|
||||
admin.oauth_scope = [:view_issues]
|
||||
assert_not admin.allowed_to?(:add_issues, project)
|
||||
assert admin.allowed_to?(:view_issues, project)
|
||||
|
||||
admin.oauth_scope = [:view_issues, :admin]
|
||||
assert admin.allowed_to?(:add_issues, project)
|
||||
assert admin.allowed_to?(:view_issues, project)
|
||||
|
||||
user = User.find 2
|
||||
project = Project.find 1
|
||||
assert user.allowed_to?(:add_issues, project)
|
||||
assert user.allowed_to?(:view_issues, project)
|
||||
user.oauth_scope = [:view_issues]
|
||||
assert_not user.allowed_to?(:add_issues, project)
|
||||
assert user.allowed_to?(:view_issues, project)
|
||||
|
||||
user.oauth_scope = [:view_issues, :admin]
|
||||
assert_not user.allowed_to?(:add_issues, project)
|
||||
assert user.allowed_to?(:view_issues, project)
|
||||
end
|
||||
|
||||
def test_destroy_should_delete_associated_reactions
|
||||
users(:users_004).reactions.create!(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user