[30007] Allow users to enable CORS on APIv3 resources

Allows a setting and configuration option to allow access to APIV3.

Setting allows us to selectively enable this for cloud instances too.

https://community.openproject.com/wp/30007
pull/8582/head
Oliver Günther 4 years ago
parent 38d2707946
commit 86b8ffc326
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 3
      Gemfile
  2. 3
      Gemfile.lock
  3. 8
      app/controllers/concerns/admin_settings_updater.rb
  4. 48
      app/controllers/settings/api_controller.rb
  5. 13
      app/helpers/settings_helper.rb
  6. 49
      app/views/settings/_api.html.erb
  7. 38
      config/initializers/rack-cors.rb
  8. 1
      config/initializers/zeitwerk.rb
  9. 6
      config/locales/en.yml
  10. 6
      config/settings.yml
  11. 9
      docs/api/apiv3/introduction.apib
  12. 8
      docs/system-admin-guide/authentication/oauth-applications/README.md
  13. 24
      docs/system-admin-guide/system-settings/api-settings/README.md
  14. 50
      lib/api/v3/cors.rb
  15. 2
      lib/open_project/cache.rb
  16. 3
      lib/open_project/static/links.rb
  17. 100
      spec/requests/api/v3/cors_header_spec.rb

@ -286,6 +286,9 @@ gem 'bootsnap', '~> 1.4.5', require: false
# API gems
gem 'grape', '~> 1.3.0'
# CORS for API
gem 'rack-cors', '~> 1.1.1'
gem 'reform', '~> 2.2.0'
gem 'reform-rails', '~> 0.1.7'
gem 'roar', '~> 1.1.0'

@ -724,6 +724,8 @@ GEM
rack (>= 0.4)
rack-attack (6.2.2)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-mini-profiler (2.0.1)
rack (>= 1.2.0)
rack-oauth2 (1.10.1)
@ -1088,6 +1090,7 @@ DEPENDENCIES
puffing-billy (~> 2.3.1)
puma (~> 4.3.5)
rack-attack (~> 6.2.2)
rack-cors (~> 1.1.1)
rack-mini-profiler
rack-protection (~> 2.0.8)
rack-test (~> 1.1.0)

@ -40,11 +40,17 @@ module AdminSettingsUpdater
if params[:settings]
Settings::UpdateService
.new(user: current_user)
.call(settings: permitted_params.settings.to_h)
.call(settings: settings_params)
flash[:notice] = t(:notice_successful_update)
redirect_to action: 'show', tab: params[:tab]
end
end
protected
def settings_params
permitted_params.settings.to_h
end
end
end

@ -0,0 +1,48 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
class Settings::ApiController < SettingsController
include AdminSettingsUpdater
menu_item :settings_api
def show
render template: 'settings/_api'
end
def default_breadcrumb
t(:label_api_access_key_type)
end
def settings_params
super.tap do |settings|
settings["apiv3_cors_origins"] = settings["apiv3_cors_origins"].split(/\r?\n/)
end
end
end

@ -50,6 +50,11 @@ module SettingsHelper
action: { controller: '/settings/projects', action: 'show' },
label: :label_project_plural
},
{
name: 'api',
action: { controller: '/settings/api', action: 'show' },
label: :label_api_access_key_type
},
{
name: 'repositories',
action: { controller: '/settings/repositories', action: 'show' },
@ -116,7 +121,13 @@ module SettingsHelper
def setting_text_area(setting, options = {})
setting_label(setting, options) +
wrap_field_outer(options) do
styled_text_area_tag("settings[#{setting}]", Setting.send(setting), options)
value = Setting.send(setting)
if value.is_a?(Array)
value = value.join("\n")
end
styled_text_area_tag("settings[#{setting}]", value, options)
end
end

@ -0,0 +1,49 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 2012-2020 the OpenProject GmbH
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-2017 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 docs/COPYRIGHT.rdoc for more details.
++#%>
<%= toolbar title: t(:label_api_access_key_type) %>
<%= styled_form_tag(update_api_settings_path, method: :patch) do %>
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend">Cross-Origin Resource Sharing (CORS)</legend>
</fieldset>
<div class="form--field">
<%= setting_check_box :apiv3_cors_enabled %>
</div>
<div class="form--field">
<%= setting_text_area :apiv3_cors_origins, rows: 5, container_class: '-wide' %>
<div class="form--field-instructions">
<p><%= t(:text_line_separated) %></p>
<p><%= t(:setting_apiv3_cors_origins_text_html,
origin_link: ::OpenProject::Static::Links[:origin_mdn_documentation][:href]) %></p>
</div>
</div>
</section>
<%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %>
<% end %>

@ -0,0 +1,38 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins { |source, env| ::API::V3::CORS.allowed?(source) }
resource '/api/v3*',
headers: :any,
methods: :any,
credentials: true,
if: proc { ::API::V3::CORS.enabled? }
end
end

@ -55,6 +55,7 @@ OpenProject::Inflector.inflection(
'scm' => 'SCM',
'imap' => 'IMAP',
'pop3' => 'POP3',
'cors' => 'CORS',
'openid_connect' => 'OpenIDConnect',
'pdf_export' => 'PDFExport'
)

@ -2161,6 +2161,12 @@ en:
search_input_placeholder: "Search ..."
setting_apiv3_cors_enabled: "Enable CORS"
setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins"
setting_apiv3_cors_origins_text_html: >
If CORS is enabled, these are the origins that are allowed to access OpenProject API.
<br/>
Please check the <a href="%{origin_link}" target="_blank">Documentation on the Origin header</a> on how to specify the expected values.
setting_email_delivery_method: "Email delivery method"
setting_sendmail_location: "Location of the sendmail executable"
setting_smtp_enable_starttls_auto: "Automatically use STARTTLS if available"

@ -365,3 +365,9 @@ installation_uuid:
oauth_allow_remapping_of_existing_users:
default: false
format: boolean
apiv3_cors_enabled:
default: false
format: boolean
apiv3_cors_origins:
serialized: true
default: []

@ -100,6 +100,15 @@ On the other hand using **API keys** has some advantages too, which is why we we
Most importantly users may not actually have a password to begin with. Specifically when they have registered
through an OpenID Connect provider.
# Cross-Origin Resource Sharing (CORS)
By default, the OpenProject API is _not_ responding with any CORS headers.
If you want to allow cross-domain AJAX calls against your OpenProject instance, you need to enable CORS headers being returned.
Please see [our API settings documentation](https://docs.openproject.org/system-admin-guide/system-settings/api-settings/) on
how to selectively enable CORS.
# Allowed HTTP methods
- `GET` - Get a single resource or collection of resources

@ -50,3 +50,11 @@ In Postman the configuration should look like this (Replace `{{protocolHostPort}
i.e. `https://example.com`)
![Sys-admin-authentication-add-oauth-application](Sys-admin-authentication-oauth-postman.png)
## CORS headers
By default, the OpenProject API is _not_ responding with any CORS headers.
If you want to allow cross-domain AJAX calls against your OpenProject instance, you need to enable CORS headers being returned.
Please see [our API settings documentation](https://docs.openproject.org/system-admin-guide/system-settings/api-settings/) on
how to selectively enable CORS.

@ -0,0 +1,24 @@
---
sidebar_navigation:
title: API settings
description: Settings for API functionality of OpenProject
robots: index, follow
keywords: API settings
---
# API system settings
In the API settings, you can selectively control whether foreign applications may access your OpenProject
API endpoints from within the browser.
## Cross-Origin Resource Sharing (CORS)
To enable CORS headers being returned by the [OpenProject APIv3](https://docs.openproject.org/api/),
enable the check box on this page.
You will then have to enter the allowed values for the Origin header that OpenProject will allow access to.
This is necessary, since authenticated resources of OpenProject cannot be accessible to all origins with the `*` header value.
For more information on the concepts of Cross-Origin Resource Sharing (CORS), please see:
- [an overview of CORS from MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
- [a tutorial on CORS by Auth0](https://auth0.com/blog/cors-tutorial-a-guide-to-cross-origin-resource-sharing/)

@ -0,0 +1,50 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
# CORS helper methods for the API v3
module API
module V3
module CORS
##
# Returns whether CORS headers should
# be set on the APIv3 resources
def self.enabled?
Setting.apiv3_cors_enabled?
end
##
# Determine whether the given origin is included
# in the allowed origin list
def self.allowed?(source)
Setting.apiv3_cors_origins.include?(source)
end
end
end
end

@ -26,6 +26,8 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
require_relative 'cache/cache_key'
module OpenProject
module Cache
def self.fetch(*parts, &block)

@ -184,6 +184,9 @@ module OpenProject
ldap_encryption_documentation: {
href: 'https://www.rubydoc.info/gems/net-ldap/Net/LDAP#constructor_details',
},
origin_mdn_documentation: {
href: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin'
},
security_badge_documentation: {
href: 'https://docs.openproject.org/system-admin-guide/information/#security-badge'
},

@ -0,0 +1,100 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 CORS headers',
type: :request,
content_type: :json do
include Rack::Test::Methods
include Capybara::RSpecMatchers
include API::V3::Utilities::PathHelper
context 'with setting enabled',
with_settings: { apiv3_cors_enabled: true } do
context 'with allowed origin set to specific values',
with_settings: { apiv3_cors_origins: %w[https://foo.example.com bla.test] } do
it 'outputs CORS headers', :aggregate_failures do
options '/api/v3',
nil,
'HTTP_ORIGIN' => 'https://foo.example.com',
'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' => 'test'
expect(last_response.headers['Access-Control-Allow-Origin']).to eq('https://foo.example.com')
expect(last_response.headers['Access-Control-Allow-Methods']).to eq('GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS')
expect(last_response.headers['Access-Control-Allow-Headers']).to eq('test')
expect(last_response.headers).to have_key('Access-Control-Max-Age')
end
it 'rejects CORS headers for invalid origin' do
options '/api/v3',
nil,
'HTTP_ORIGIN' => 'invalid.example.com',
'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' => 'test'
expect(last_response.headers).not_to have_key 'Access-Control-Allow-Origin'
expect(last_response.headers).not_to have_key 'Access-Control-Allow-Methods'
expect(last_response.headers).not_to have_key 'Access-Control-Allow-Headers'
expect(last_response.headers).not_to have_key 'Access-Control-Max-Age'
end
# CORS needs to output headers even if you're unauthorized to allow authentication
# to happen
it 'returns the CORS header on an unauthorized resource as well', :aggregate_failures do
options '/api/v3/work_packages/form',
nil,
'HTTP_ORIGIN' => 'https://foo.example.com'
expect(last_response.headers['Access-Control-Allow-Origin']).to eq('https://foo.example.com')
expect(last_response.headers['Access-Control-Allow-Methods']).to eq('GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS')
expect(last_response.headers).to have_key('Access-Control-Max-Age')
end
end
end
context 'when disabled',
with_settings: { apiv3_cors_enabled: false, apiv3_cors_origins: %w[foo.example.com] } do
it 'does not output CORS headers even though origin matches', :aggregate_failures do
options '/api/v3',
nil,
'HTTP_ORIGIN' => 'foo.example.com',
'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' => 'test'
expect(last_response.headers).not_to have_key 'Access-Control-Allow-Origin'
expect(last_response.headers).not_to have_key 'Access-Control-Allow-Methods'
expect(last_response.headers).not_to have_key 'Access-Control-Allow-Headers'
expect(last_response.headers).not_to have_key 'Access-Control-Max-Age'
end
end
end
Loading…
Cancel
Save