Merge branch 'release/4.3' into dev

pull/3218/head
Jan Sandbrink 9 years ago
commit 0418507108
  1. 4
      Gemfile
  2. 17
      Gemfile.lock
  3. 10
      app/controllers/application_controller.rb
  4. 13
      app/controllers/projects_controller.rb
  5. 7
      app/models/project.rb
  6. 4
      app/views/projects/_edit.html.erb
  7. 2
      app/views/projects/_form.html.erb
  8. 23
      config/initializers/warden.rb
  9. 1717
      config/locales/de.yml
  10. 414
      config/locales/js-de.yml
  11. 2
      frontend/app/helpers/path-helper.js
  12. 3
      frontend/app/openproject-app.js
  13. 8
      lib/api/root.rb
  14. 5
      lib/api/utilities/grape_helper.rb
  15. 94
      lib/open_project/authentication.rb
  16. 53
      lib/open_project/authentication/manager.rb
  17. 44
      spec/controllers/api/v2/authentication_spec.rb
  18. 36
      spec/requests/api/v3/authentication_spec.rb

@ -230,6 +230,10 @@ platforms :jruby do
end
end
group :opf_plugins do
gem 'openproject-translations', git:'https://github.com/opf/openproject-translations.git', branch: 'release/4.2'
end
# Load Gemfile.local, Gemfile.plugins and plugins' Gemfiles
Dir.glob File.expand_path("../{Gemfile.local,Gemfile.plugins,lib/plugins/*/Gemfile}", __FILE__) do |file|
next unless File.readable?(file)

@ -40,6 +40,17 @@ GIT
sass (>= 3.2.0)
sprockets-rails (~> 2.0.0.backport1)
GIT
remote: https://github.com/opf/openproject-translations.git
revision: 99f40603ca7778855eddf9510538070827254423
branch: release/4.2
specs:
openproject-translations (4.2.0.pre.alpha)
crowdin-api (~> 0.2.4)
mixlib-shellout (~> 2.1.0)
rails (~> 3.2.14)
rubyzip
GIT
remote: https://github.com/rails/prototype_legacy_helper.git
revision: a2cd95c3e3c1a4f7a9566efdab5ce59c886cb05f
@ -151,6 +162,8 @@ GEM
color-tools (1.3.0)
colored (1.2)
columnize (0.8.9)
crowdin-api (0.2.8)
rest-client (~> 1.6.8)
cucumber (1.3.18)
builder (>= 2.1.2)
diff-lcs (>= 1.1.3)
@ -264,6 +277,7 @@ GEM
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.6.2)
mixlib-shellout (2.1.0)
multi_json (1.11.0)
multi_test (0.1.1)
multi_xml (0.5.5)
@ -361,6 +375,8 @@ GEM
nokogiri
uber (~> 0.0.7)
request_store (1.1.0)
rest-client (1.6.9)
mime-types (~> 1.16)
roar (1.0.1)
representable (>= 2.0.1, <= 3.0.0)
rspec (3.3.0)
@ -514,6 +530,7 @@ DEPENDENCIES
object-daddy (~> 1.1.0)
oj (~> 2.11.4)
omniauth
openproject-translations!
pg (~> 0.17.1)
prototype-rails
prototype_legacy_helper (= 0.0.0)!

@ -256,15 +256,13 @@ class ApplicationController < ActionController::Base
respond_to do |format|
format.any(:html, :atom) { redirect_to signin_path(back_url: url) }
authentication_scheme = if request.headers['X-Authentication-Scheme'] == 'Session'
'Session'
else
'Basic'
end
auth_header = OpenProject::Authentication::WWWAuthenticate.response_header(
request_headers: request.headers)
format.any(:xml, :js, :json) do
head :unauthorized,
'X-Reason' => 'login needed',
'WWW-Authenticate' => authentication_scheme + ' realm="OpenProject API"'
'WWW-Authenticate' => auth_header
end
end
return false

@ -123,19 +123,24 @@ class ProjectsController < ApplicationController
end
def settings
@altered_project ||= @project
end
def edit
end
def update
@project.safe_attributes = params[:project]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
@altered_project = Project.find(@project.id)
@altered_project.safe_attributes = params[:project]
if validate_parent_id && @altered_project.save
if params[:project].has_key?('parent_id')
@altered_project.set_allowed_parent!(params[:project]['parent_id'])
end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_to action: 'settings', id: @project
redirect_to action: 'settings', id: @altered_project
}
end
else

@ -105,9 +105,10 @@ class Project < ActiveRecord::Base
validates_length_of :name, maximum: 255
validates_length_of :homepage, maximum: 255
validates_length_of :identifier, in: 1..IDENTIFIER_MAX_LENGTH
# downcase letters, digits, dashes but not digits only
validates_format_of :identifier, with: /\A(?!\d+$)[a-z0-9\-_]*\z/,
if: -> (p) { p.identifier_changed? }
# starts with lower-case letter, a-z, 0-9, dashes and underscores afterwards
validates :identifier,
format: { with: /\A[a-z][a-z0-9\-_]*\z/ },
if: -> (p) { p.identifier_changed? }
# reserved words
validates_exclusion_of :identifier, in: RESERVED_IDENTIFIERS

@ -26,9 +26,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See doc/COPYRIGHT.rdoc for more details.
++#%>
<%= labelled_tabular_form_for @project do |f| %>
<%= labelled_tabular_form_for @altered_project, url: project_path(@project) do |f| %>
<%= render partial: 'form', locals: { f: f,
project: @project,
project: @altered_project,
renderTypes: false } %>
<%= f.button l(:button_save), class: 'button -highlight -with-icon icon-yes' %>

@ -27,7 +27,7 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<%= error_messages_for 'project' %>
<%= error_messages_for project %>
<!--[form:project]-->

@ -6,19 +6,24 @@ require 'open_project/authentication/strategies/warden/global_basic_auth'
require 'open_project/authentication/strategies/warden/user_basic_auth'
require 'open_project/authentication/strategies/warden/session'
strategies = {
basic_auth_failure: OpenProject::Authentication::Strategies::Warden::BasicAuthFailure,
global_basic_auth: OpenProject::Authentication::Strategies::Warden::GlobalBasicAuth,
user_basic_auth: OpenProject::Authentication::Strategies::Warden::UserBasicAuth,
session: OpenProject::Authentication::Strategies::Warden::Session
}
WS = OpenProject::Authentication::Strategies::Warden
strategies = [
[:basic_auth_failure, WS::BasicAuthFailure, 'Basic'],
[:global_basic_auth, WS::GlobalBasicAuth, 'Basic'],
[:user_basic_auth, WS::UserBasicAuth, 'Basic'],
[:session, WS::Session, 'Session']
]
strategies.each do |name, clazz|
Warden::Strategies.add name, clazz
strategies.each do |name, clazz, auth_scheme|
OpenProject::Authentication.add_strategy name, clazz, auth_scheme
end
include OpenProject::Authentication::Scope
OpenProject::Authentication.update_strategies(API_V3) do |_strategies|
api_v3_options = {
store: false
}
OpenProject::Authentication.update_strategies(API_V3, api_v3_options) do |_strategies|
[:global_basic_auth, :user_basic_auth, :basic_auth_failure, :session]
end

File diff suppressed because it is too large Load Diff

@ -1,414 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
de:
js:
ajax:
hide: "Verbergen"
loading: "Lädt ..."
close_popup_title: "Dialog schließen"
button_add_watcher: "Beobachter hinzufügen"
button_cancel: "Abbrechen"
button_check_all: "Alles auswählen"
button_copy: "Kopieren"
button_delete: "Löschen"
button_delete_watcher: "Lösche Beobachter"
button_details_view: "Detailansicht"
button_duplicate: "Duplizieren"
button_edit: "Bearbeiten"
button_filter: "Filter"
button_list_view: "Listenansicht"
button_log_time: "Aufwand buchen"
button_more: "Mehr"
button_move: "Verschieben"
button_open_details: "Öffne Detailansicht"
button_quote: "Zitieren"
button_save: "Speichern"
button_settings: "Einstellungen"
button_uncheck_all: "Alles abwählen"
button_update: "Bearbeiten"
description_available_columns: "Verfügbare Spalten"
description_select_work_package: "Arbeitspaket auswählen"
description_selected_columns: "Ausgewählte Spalten"
description_subwork_package: "Untergeordnetes Arbeitspaket von"
filter:
noneElement: "(keines)"
general_text_no: "nein"
general_text_yes: "ja"
general_text_No: "Nein"
general_text_Yes: "Ja"
label_activate: "Aktiviere"
label_add_columns: "Ausgewählte Spalten hinzufügen"
label_add_comment: "Kommentar hinzufügen"
label_add_comment_title: "Fügen Sie Ihre Kommentare hier hinzu"
label_added_by: "hinzugefügt von"
label_added_time_by: "Von %{author} %{age} hinzugefügt"
label_ago: "vor"
label_all: "alle"
label_all_work_packages: "alle Arbeitspakete"
label_ascending: "Aufsteigend"
label_board_locked: "Gesperrt"
label_board_sticky: "Wichtig (immer oben)"
label_closed_work_packages: "geschlossen"
label_collapse: "Zuklappen"
label_collapsed: "zugeklappt"
label_collapse_all: "Alle zuklappen"
label_commented_on: "kommentiert am"
label_contains: "enthält"
label_date: "Datum"
label_deactivate: "Deaktiviere"
label_descending: "Absteigend"
label_description: "Beschreibung"
label_equals: "ist"
label_expand: "Aufklappen"
label_expanded: "aufgeklappt"
label_expand_all: "Alle aufklappen"
label_export: "Exportieren"
label_filename: "Datei"
label_filesize: "Größe"
label_format_atom: "Atom"
label_format_csv: "CSV"
label_format_pdf: "PDF"
label_format_pdf_with_descriptions: "PDF mit Beschreibungen"
label_greater_or_equal: ">="
label_group_by: "Gruppierung"
label_hide_attributes: "Leere ausblenden"
label_hide_column: "Spalte ausblenden"
label_in: "an"
label_in_less_than: "in weniger als"
label_in_more_than: "in mehr als"
label_latest_activity: "Letzte Änderungen"
label_last_updated_on: "Zuletzt aktualisiert am"
label_less_or_equal: "<="
label_less_than_ago: "vor weniger als"
label_loading: "Lade..."
label_me: "ich"
label_menu_collapse: "ausblenden"
label_menu_expand: "einblenden"
label_more_than_ago: "vor mehr als"
label_next: "Weiter"
label_no_data: "Nichts anzuzeigen"
label_no_due_date: "kein Abgabedatum"
label_no_start_date: "kein Startdatum"
label_none: "kein"
label_not_contains: "enthält nicht"
label_not_equals: "ist nicht"
label_on: "am"
label_open_menu: "Menü öffnen"
label_open_work_packages: "offen"
label_previous: "Zurück"
label_per_page: "Pro Seite:"
label_remove_columns: "Ausgewählte Spalten entfernen"
label_save_as: "Speichern unter"
label_select_watcher: "Wählen Sie einen Beobachter..."
label_selected_filter_list: "Ausgewählte Filter"
label_share: "Sichtbarkeit"
label_show_attributes: "Alle anzeigen"
label_show_in_menu: "Seite in Projektnavigation anzeigen"
label_sort_by: "Sortiere nach"
label_sorted_by: "sortiert nach"
label_sort_higher: "Eins höher"
label_sort_lower: "Eins tiefer"
label_sorting: "Sortierung"
label_status: "Status"
label_sum_for: "Summe für"
label_this_week: "aktuelle Woche"
label_today: "heute"
label_total_progress: "%{percent}% Gesamtfortschritt"
label_visible_for_others: "Seite sichtbar für andere Nutzer"
label_work_package: "Arbeitspaket"
label_watch_work_package: "Arbeitspaket beobachten"
label_watcher_added_successfully: "Beobachter wurde erfolgreich hinzugefügt!"
label_watcher_deleted_successfully: "Beobachter wurde erfolgreich entfernt!"
label_work_package_details_you_are_here: "Sie sind auf dem Reiter %{tab} von %{type} %{subject}."
label_unwatch_work_package: "Arbeitspaket nicht beobachten"
label_uploaded_by: "Hochgeladen von"
label_global_queries: "Gemeinsame Filter"
label_custom_queries: "Meine Filter"
label_columns: "Spalten"
label_click_to_enter_description: "Klicken um die Beschreibung einzugeben..."
text_are_you_sure: "Sind Sie sicher?"
filter_labels:
assigned_to: "Zugewiesen an"
assigned_to_role: "Zuständigkeitsrolle"
author: "Autor"
category: "Kategorie"
created_at: "Angelegt"
done_ratio: "% erledigt"
due_date: "Abgabedatum"
estimated_hours: "Geschätzter Aufwand"
fixed_version: "Version"
member_of_group: "Zuständigkeitsgruppe"
parent: "Übergeordnetes Arbeitspaket"
parent_issue: "Übergeordnetes Arbeitspaket"
parent_work_package: "Übergeordnetes Arbeitpaket"
priority: "Priorität"
progress: "% erledigt"
project: "Projekt"
responsible: "Verantwortlicher"
spent_time: "Aufgewendete Zeit"
subproject: "Unterprojekt"
start_date: "Beginn"
status: "Status"
subject: "Thema"
time_entries: "Logzeit"
type: "Typ"
updated_at: "Aktualisiert"
version: "Version"
watcher: "Beobachter"
relation_labels:
parent: "Übergeordnetes Arbeitspaket"
children: "Untergeordnetes Arbeitspaket"
relates: "Beziehung mit"
duplicates: "Dupliziert"
duplicated: "Dupliziert durch"
blocks: "Blockiert"
blocked: "Blockiert durch"
precedes: "Vorgänger von"
follows: "Folgt"
relation_buttons:
change_parent: "Übergeordnetes Arbeitspaket ändern"
add_child: "Untergeordnetes Arbeitspaket hinzufügen"
add_related_to: "Beziehung mit hinzufügen"
add_duplicates: "Duplikat von hinzufügen"
add_duplicated_by: "Dupliziert durch hinzufügen"
add_blocks: "Blockiert hinzufügen"
add_blocked_by: "Blockiert durch hinzufügen"
add_precedes: "Vorgänger von hinzufügen"
add_follows: "Folgt hinzufügen"
field_value_enter_prompt: "Einen Wert für '%{field}' eingeben"
select2:
input_too_short:
one: "Bitte geben Sie ein weiteres Zeichen ein"
other: "Bitte geben Sie {{count}} weitere Zeichen ein"
zero: "Bitte geben Sie weitere Zeichen ein"
load_more: "Mehr Ergebnisse werden geladen ..."
no_matches: "Keine Treffer"
searching: "Suche ..."
selection_too_big:
one: "Sie dürfen nur ein Element auswählen"
other: "Sie dürfen nur {{limit}} Elemente auswählen"
zero: "Sie dürfen keine Elemente auswählen"
text_work_packages_destroy_confirmation: "Sind Sie sicher, dass Sie die ausgewählten Arbeitspakete löschen möchten?"
text_query_destroy_confirmation: "Möchten Sie die ausgewählte Abfrage wirklich löschen?"
timelines:
cancel: "Abbrechen"
change: "Planungsveränderung"
due_date: "Abschlussdatum"
empty: "(leer)"
error: "Ein Fehler ist aufgetreten."
errors:
report_timeout: "Zeitüberschreitung beim Laden des Zeitplans."
filter:
column:
assigned_to: "Zugewiesen an"
type: "Typ"
due_date: "Abschlussdatum"
name: "Name"
status: "Status"
responsible: "Verantwortlicher"
start_date: "Startdatum"
grouping_other: "Andere"
noneSelection: "(keine)"
name: "Name"
new_work_package: "Neues Arbeitspaket"
outline: "Hierarchie zurücksetzen"
outlines:
aggregation: "Nur Aggregationen anzeigen"
level1: "Bis zur ersten Ebene"
level2: "Bis zur zweiten Ebene"
level3: "Bis zur dritten Ebene"
level4: "Bis zur vierten Ebene"
level5: "Bis zur fünften Ebene"
all: "Alle Ebenen anzeigen"
project_status: "Projekt-Status"
project_type: "Projekt-Typ"
really_close_dialog: "Dialog schließen und eingegebene Daten verwerfen?"
responsible: "Verantwortlicher"
save: "Speichern"
start_date: "Startdatum"
tooManyProjects: "Mehr als %{count} Projekte. Bitte genauer filtern!"
zoom:
in: "Mehr Details"
out: "Weniger Details"
days: "Tage"
weeks: "Wochen"
months: "Monate"
quarters: "Quartale"
years: "Jahre"
slider: "Detail Schieber"
tl_toolbar:
zooms: "Zoomstufe"
outlines: "Hierarchie-Stufe"
unsupported_browser:
title: "Ihre Browserversion wird nicht unterstützt"
message: "Sie verwenden einen veralteten Browser. OpenProject unterstützt diese Version des Browsers nicht länger. Bitte aktualisieren Sie Ihren Browser."
learn_more: "Mehr erfahren"
wiki_formatting:
strong: "Fett"
italic: "Kursiv"
underline: "Unterstrichen"
deleted: "Duchgestrichen"
code: "Quelltext"
heading1: "Überschrift 1. Ordnung"
heading2: "Überschrift 2. Ordnung"
heading3: "Überschrift 3. Ordnung"
unordered_list: "Aufzählungsliste"
ordered_list: "Nummerierte Liste"
quote: "Zitieren"
unquote: "Zitat entfernen"
preformatted_text: "Präformatierter Text"
wiki_link: "Verweis (Link) zu einer Wiki-Seite"
image: "Grafik"
work_packages:
button_clear: "Zurücksetzen"
description_filter: "Filter"
description_enter_text: "Text eingeben"
description_options_hide: "Optionen ausblenden"
description_options_show: "Optionen einblenden"
label_enable_multi_select: "Mehrfachauswahl aktivieren"
label_disable_multi_select: "Mehrfachauswahl deaktivieren"
label_column_multiselect: "Kombiniertes Eingabefeld: Auswahl mit Pfeiltasten, Auslösen mit Enter, Löschen mit Backspace"
label_column_select: "Status - Kombiniertes Eingabefeld: Auswahl mit Autovervollständigung"
label_filter_add: "Filter hinzufügen"
label_options: "Optionen"
message_error_during_bulk_delete: Fehler beim Löschen der Arbeitspakete.
message_successful_bulk_delete: Arbeitspakete erfolgreich gelöscht.
no_results:
title: Keine Arbeitspakete anzuzeigen
description_html: |
<p>Es wurden entweder keine Arbeitspakete erzeugt oder alle Arbeitspakete wurden ausgefiltert.</p>
property_groups:
details: "Details"
people: "Personen"
estimatesAndTime: "Schätzungen & Zeit"
other: "Andere"
properties:
assignee: "Zugewiesen an"
author: "Autor"
createdAt: "Angelegt"
description: "Beschreibung"
date: "Datum"
dueDate: "Abgabedatum"
estimatedTime: "Geschätzter Aufwand"
spentTime: "Aufgewendete Zeit"
category: "Kategorie"
percentageDone: "% erledigt"
priority: "Priorität"
projectName: "Projekt"
responsible: "Verantwortlicher"
startDate: "Startdatum"
status: "Status"
subject: "Thema"
title: "Titel"
type: "Typ"
updatedAt: "Aktualisiert"
versionName: "Version"
version: "Version"
query:
column_names: "Spalten"
group_by: "Gruppiere Ergebnisse nach"
group: "Gruppiere"
sort_ascending: "Sortiere aufsteigend"
sort_descending: "Sortiere absteigend"
move_column_left: "Spalte nach links"
move_column_right: "Spalte nach rechts"
hide_column: "Spalte verbergen"
insert_columns: "Spalten hinzufügen ..."
filters: "Filter"
display_sums: "Summen anzeigen"
errors:
unretrievable_query: "Die URL enthält keine benutzerdefinierte Abfrage"
tabs:
overview: "Übersicht"
activity: "Aktivität"
relations: "Beziehungen"
watchers: "Beobachter"
attachments: "Anhänge"
time_relative:
days: "Tagen"
weeks: "Wochen"
months: "Monaten"
toolbar:
settings:
columns: "Spalten ..."
sort_by: "Sortiere nach ..."
group_by: "Gruppiere nach ..."
display_sums: "Summen anzeigen"
hide_sums: "Summen nicht anzeigen"
save: "Speichern"
save_as: "Speichern unter ..."
export: "Exportieren ..."
share: "Sichtbarkeit ..."
page_settings: "Filter umbenennen ..."
delete: "Löschen"
filter: "Filter"
unselected_title: "Arbeitspakete"
modals:
label_settings: "Filter umbenennen"
label_name: "Name"
label_delete_page: "Aktuelle Seite löschen"
button_apply: "Anwenden"
button_save: "Speichern"
button_submit: "OK"
button_cancel: "Abbrechen"
notice_successful_create: "Erfolgreich angelegt"
notice_successful_delete: "Erfolgreich gelöscht."
notice_successful_update: "Erfolgreich aktualisiert."
notice_bad_request: "Fehlerhafte Anfrage."
relations:
empty: Keine bestehenden Beziehungen
delete: Beziehung löschen
inplace:
button_edit: "%{attribute} bearbeiten"
button_save: "%{attribute}: Speichern"
button_save_and_send: "%{attribute} Speichern mit E-Mail-Benachrichtigung"
button_cancel: "%{attribute} Abbrechen"
link_formatting_help: "Textformatierung"
btn_preview_enable: "Vorschau"
btn_preview_disable: "Vorschau deaktivieren"
null_value_label: "Kein Wert"
clear_value_label: "-"
errors:
required: '%{field} ist ein Pflichtfeld'
number: '%{field} ist keine gültige Zahl'
error_could_not_resolve_version_name: "Versionsbezeichner konnte nicht aufgelöst werden"
error_could_not_resolve_user_name: "Benutzername konnte nicht aufgelöst werden"
units:
hour:
one: "1 Stunde"
other: "%{count} Stunden"
zero: "0 Stunden"

@ -85,7 +85,7 @@ module.exports = function() {
return path;
},
timeEntryPath: function(timeEntryIdentifier) {
return '/time_entries/' + timeEntryIdentifier;
return PathHelper.staticBase + '/time_entries/' + timeEntryIdentifier;
},
timeEntryNewPath: function(workPackageId) {
return PathHelper.timeEntriesPath(null, workPackageId) + '/new';

@ -30,7 +30,6 @@ var I18n = require('./vendor/i18n');
// standard locales
I18n.translations.en = require("locales/js-en.yml").en;
I18n.translations.de = require("locales/js-de.yml").de;
I18n.addTranslations = function(locale, translations) {
I18n.translations[locale] = _.merge(I18n.translations[locale], translations);
@ -195,7 +194,7 @@ openprojectApp
$locationProvider.html5Mode(true);
$httpProvider.defaults.headers.common['X-CSRF-TOKEN'] = jQuery(
'meta[name=csrf-token]').attr('content'); // TODO find a more elegant way to keep the session alive
$httpProvider.defaults.headers.common['X-Authentication-Scheme'] = 'Session';
// prepend a given base path to requests performed via $http
//
// NOTE: this does not apply to Hyperagent-based queries, which instead use

@ -147,7 +147,13 @@ module API
end
def self.auth_headers
{ 'WWW-Authenticate' => %(Basic realm="#{OpenProject::Authentication::Realm.realm}") }
lambda do
header = OpenProject::Authentication::WWWAuthenticate.response_header(
scope: API_V3,
request_headers: env)
{ 'WWW-Authenticate' => header }
end
end
##

@ -48,12 +48,13 @@ module API
end
end
def error_response(rescued_error, error = nil, rescue_subclasses: nil, headers: {})
def error_response(rescued_error, error = nil, rescue_subclasses: nil, headers: ->() { {} })
default_response = lambda do |e|
representer = ::API::V3::Errors::ErrorRepresenter.new e
resp_headers = instance_exec &headers
env['api.format'] = 'hal+json'
error_response status: e.code, message: representer.to_json, headers: headers
error_response status: e.code, message: representer.to_json, headers: resp_headers
end
response =

@ -5,26 +5,41 @@ module OpenProject
# OpenProject uses Warden strategies for request authentication.
module Authentication
class << self
##
# Registers a given Warden strategy to be used for authentication.
#
# @param [Symbol] Name under which the strategy can be referred to.
# @param [Class] The strategy class.
# @param [String] The authentication scheme implemented by this strategy.
# Used in the WWW-Authenticate header in 401 responses.
def add_strategy(name, clazz, auth_scheme)
Warden::Strategies.add name, clazz
info = Manager.auth_scheme auth_scheme
info.strategies << name
end
##
# Updates the used warden strategies for a given scope. The strategies will be tried
# in the order they are set here. Plugins can call this to add or remove strategies.
# For available scopes please refer to `OpenProject::Authentication::Scope`.
#
# @param [Symbol] scope The scope for which to update the used warden strategies.
# @param [Boolean] store Indicates whether the user should be stored in the session
# for this scope. Optional. If not given, the current store flag
# for this strategy will remain unchanged what ever it is.
# @param [Hash] opts Options for that scope.
# @option opts [Boolean] :store Indicates whether the user should be stored in the session
# for this scope. Optional. If not given, the current store
# flag for this strategy will remain unchanged what ever it is.
# @option opts [String] :realm The WWW-Authenticate realm used for authentication challenges
# for this scope. The default value ()
#
# @yield [strategies] A block returning the strategies to be used for this scope.
# @yieldparam [Array] strategies The strategies currently used by this scope. May be empty.
# @yieldreturn [Array] The strategies to be used by this scope.
def update_strategies(scope, store: nil, &block)
# @yieldparam [Set] strategies The strategies currently used by this scope. May be empty.
# @yieldreturn [Set] The strategies to be used by this scope.
def update_strategies(scope, opts = {}, &block)
raise ArgumentError, "invalid scope: #{scope}" unless Scope.values.include? scope
current_strategies = Array(Manager.scope_strategies[scope])
Manager.store_defaults[scope] = store unless store.nil?
Manager.scope_strategies[scope] = block.call current_strategies if block_given?
config = Manager.scope_config scope
config.update! opts, &block
end
##
@ -61,14 +76,67 @@ module OpenProject
end
end
module Realm
##
# Options used in the WWW-Authenticate header returned to the user
# in case authentication failed (401).
module WWWAuthenticate
module_function
def pick_auth_scheme(supported_schemes, default_scheme, request_headers = {})
req_scheme = request_headers['X-Authentication-Scheme']
if supported_schemes.include? req_scheme
req_scheme
else
default_scheme
end
end
def default_auth_scheme
'Basic'
end
def default_realm
'OpenProject API'
end
def scope_realm(scope = nil)
Manager.scope_config(scope).realm || default_realm
end
def response_header(
default_auth_scheme: self.default_auth_scheme,
scope: nil,
request_headers: {}
)
scheme = pick_auth_scheme auth_schemes(scope), default_auth_scheme, request_headers
"#{scheme} realm=\"#{scope_realm(scope)}\""
end
def auth_schemes(scope)
strategies = Array(Manager.scope_config(scope).strategies)
Manager.auth_schemes
.select { |_, info| scope.nil? or not (info.strategies & strategies).empty? }
.keys
end
end
module AuthHeaders
include WWWAuthenticate
# #scope available from Warden::Strategies::BasicAuth
def auth_scheme
pick_auth_scheme auth_schemes(scope), default_auth_scheme, env
end
def realm
'OpenProject'
scope_realm scope
end
end
end
end
Warden::Strategies::BasicAuth.prepend OpenProject::Authentication::Realm
Warden::Strategies::BasicAuth.prepend OpenProject::Authentication::AuthHeaders

@ -1,3 +1,5 @@
require 'set'
module OpenProject
module Authentication
class Manager < Warden::Manager
@ -11,7 +13,7 @@ module OpenProject
def initialize(app, options = {}, &configure)
block = lambda do |config|
self.class.configure config
self.class.configure_warden config
configure.call config if configure
end
@ -20,27 +22,58 @@ module OpenProject
end
class << self
def scope_strategies
@scope_strategies ||= {}
def config
@config ||= Hash.new
end
def store_defaults
@store_defaults ||= Hash.new false
def scope_config(scope)
config[scope] ||= ScopeSettings.new
end
def failure_handlers
@failure_handlers ||= {}
end
def configure(config)
config.default_strategies :session
config.failure_app = OpenProject::Authentication::FailureApp.new failure_handlers
def auth_scheme(name)
auth_schemes[name] ||= AuthSchemeInfo.new
end
def auth_schemes
@auth_schemes ||= {}
end
def configure_warden(warden_config)
warden_config.default_strategies :session
warden_config.failure_app = OpenProject::Authentication::FailureApp.new failure_handlers
scope_strategies.each do |scope, strategies|
config.scope_defaults scope, strategies: strategies, store: store_defaults[scope]
config.each do |scope, cfg|
warden_config.scope_defaults scope, strategies: cfg.strategies, store: cfg.store
end
end
end
class ScopeSettings
attr_accessor :store, :strategies, :realm
def initialize
@store = true
@strategies = Set.new
end
def update!(opts, &block)
self.store = opts[:store] if opts.include? :store
self.realm = opts[:realm] if opts.include? :realm
self.strategies = block.call self.strategies if block_given?
end
end
class AuthSchemeInfo
attr_accessor :strategies
def initialize
@strategies = Set.new
end
end
end
end
end

@ -100,4 +100,48 @@ describe Api::V2::AuthenticationController, type: :controller do
end
end
end
describe 'WWW-Authenticate response header upon failure' do
let(:api_key) { user.api_key }
let(:user) { FactoryGirl.create(:admin) }
let(:ttl) { 42 }
before do
allow(Setting).to receive(:login_required?).and_return true
allow(Setting).to receive(:rest_api_enabled?).and_return true
end
it 'has Basic auth_scheme per default' do
get :index, format: 'xml', key: api_key.reverse
expect(response.status).to eq 401
expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="OpenProject API"'
end
context 'with Session auth scheme requested' do
before do
request.env['X-Authentication-Scheme'] = 'Session'
end
it 'has Session auth scheme' do
get :index, format: 'xml', key: api_key.reverse
expect(response.status).to eq 401
expect(response.headers['WWW-Authenticate']).to eq 'Session realm="OpenProject API"'
end
end
context 'with another default realm' do
before do
allow(OpenProject::Authentication::WWWAuthenticate)
.to receive(:default_realm).and_return 'Narnia'
end
it 'has another realm' do
get :index, format: 'xml', key: api_key.reverse
expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="Narnia"'
end
end
end
end

@ -66,7 +66,8 @@ describe API::V3, type: :request do
end
it 'should return the WWW-Authenticate header' do
expect(response.header['WWW-Authenticate']).to include 'Basic realm="OpenProject"'
expect(response.header['WWW-Authenticate'])
.to include 'Basic realm="OpenProject API"'
end
end
@ -90,7 +91,38 @@ describe API::V3, type: :request do
end
it 'should return the WWW-Authenticate header' do
expect(response.header['WWW-Authenticate']).to include 'Basic realm="OpenProject"'
expect(response.header['WWW-Authenticate'])
.to include 'Basic realm="OpenProject API"'
end
end
context 'with invalid credentials an X-Authentication-Scheme "Session"' do
let(:expected_message) { 'You did not provide the correct credentials.' }
let(:headers) do
auth = basic_auth(username, password.reverse)
auth.merge('X-Authentication-Scheme' => 'Session')
end
before do
get resource, {}, headers
end
it 'should return 401 unauthorized' do
expect(response.status).to eq 401
end
it 'should return the correct JSON response' do
expect(JSON.parse(response.body)).to eq response_401
end
it 'should return the correct content type header' do
expect(response.headers['Content-Type']).to eq 'application/hal+json; charset=utf-8'
end
it 'should return the WWW-Authenticate header' do
expect(response.header['WWW-Authenticate'])
.to include 'Session realm="OpenProject API"'
end
end

Loading…
Cancel
Save