Merge pull request #7869 from opf/feature/bcf_api_projects

Feature/bcf api projects

[ci skip]
pull/7872/head
Oliver Günther 5 years ago committed by GitHub
commit b835a18851
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      app/contracts/projects/base_contract.rb
  2. 1
      app/controllers/oauth/auth_base_controller.rb
  3. 80
      app/services/api/parse_resource_params_service.rb
  4. 40
      app/services/api/v3/parse_resource_params_service.rb
  5. 20
      app/views/oauth/applications/_form.html.erb
  6. 5
      config/locales/en.yml
  7. 2
      config/routes.rb
  8. 43
      lib/api/formatter.rb
  9. 229
      lib/api/root.rb
  10. 244
      lib/api/root_api.rb
  11. 172
      lib/api/utilities/endpoints/bodied.rb
  12. 50
      lib/api/utilities/endpoints/create.rb
  13. 107
      lib/api/utilities/endpoints/delete.rb
  14. 74
      lib/api/utilities/endpoints/index.rb
  15. 88
      lib/api/utilities/endpoints/modify.rb
  16. 86
      lib/api/utilities/endpoints/show.rb
  17. 47
      lib/api/utilities/endpoints/update.rb
  18. 6
      lib/api/utilities/grape_helper.rb
  19. 5
      lib/api/v3/errors/error_representer.rb
  20. 42
      lib/api/v3/parser.rb
  21. 6
      lib/api/v3/projects/projects_api.rb
  22. 170
      lib/api/v3/utilities/endpoints/bodied.rb
  23. 18
      lib/api/v3/utilities/endpoints/create.rb
  24. 73
      lib/api/v3/utilities/endpoints/delete.rb
  25. 4
      lib/api/v3/utilities/endpoints/form.rb
  26. 24
      lib/api/v3/utilities/endpoints/index.rb
  27. 97
      lib/api/v3/utilities/endpoints/modify.rb
  28. 53
      lib/api/v3/utilities/endpoints/show.rb
  29. 15
      lib/api/v3/utilities/endpoints/update.rb
  30. 51
      lib/api/v3/utilities/endpoints/v3_deductions.rb
  31. 44
      lib/api/v3/utilities/endpoints/v3_present_single.rb
  32. 5
      lib/open_project/static_routing.rb
  33. 56
      modules/bcf/app/controllers/bcf/api/root.rb
  34. 39
      modules/bcf/app/controllers/bcf/api/v2_1/auth_api.rb
  35. 38
      modules/bcf/app/controllers/bcf/api/v2_1/current_user_api.rb
  36. 57
      modules/bcf/app/controllers/bcf/api/v2_1/endpoints/index.rb
  37. 44
      modules/bcf/app/controllers/bcf/api/v2_1/endpoints/show.rb
  38. 51
      modules/bcf/app/controllers/bcf/api/v2_1/endpoints/update.rb
  39. 57
      modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb
  40. 43
      modules/bcf/app/formatters/bcf/api/error_formatter/json.rb
  41. 56
      modules/bcf/app/representers/bcf/api/v2_1/auth/single_representer.rb
  42. 39
      modules/bcf/app/representers/bcf/api/v2_1/errors/error_representer.rb
  43. 40
      modules/bcf/app/representers/bcf/api/v2_1/projects/single_representer.rb
  44. 40
      modules/bcf/app/representers/bcf/api/v2_1/users/single_representer.rb
  45. 46
      modules/bcf/app/services/bcf/api/v2_1/parse_resource_params_service.rb
  46. 5
      modules/bcf/config/locales/en.yml
  47. 2
      modules/bcf/config/routes.rb
  48. 14
      modules/bcf/lib/open_project/bcf/engine.rb
  49. 148
      modules/bcf/spec/features/bcf_api_authorization.rb
  50. 74
      modules/bcf/spec/representers/bcf/api/v2_1/auth/single_representer_rendering_spec.rb
  51. 55
      modules/bcf/spec/representers/bcf/api/v2_1/projects/single_representer_rendering_spec.rb
  52. 8
      modules/bcf/spec/representers/bcf/api/v2_1/shared_examples.rb
  53. 55
      modules/bcf/spec/representers/bcf/api/v2_1/users/single_representer_rendering_spec.rb
  54. 62
      modules/bcf/spec/requests/api/bcf/v2_1/auth_api_spec.rb
  55. 60
      modules/bcf/spec/requests/api/bcf/v2_1/current_user_api_spec.rb
  56. 156
      modules/bcf/spec/requests/api/bcf/v2_1/projects_api_spec.rb
  57. 107
      modules/bcf/spec/requests/api/bcf/v2_1/shared_responses.rb
  58. 1
      spec/features/admin/oauth/oauth_applications_management_spec.rb

@ -92,7 +92,9 @@ module Projects
end
def validate_user_allowed_to_manage
errors.add :base, :error_unauthorized unless user.allowed_to?(manage_permission, model)
with_unchanged_id do
errors.add :base, :error_unauthorized unless user.allowed_to?(manage_permission, model)
end
end
def validate_status_code_included
@ -102,5 +104,14 @@ module Projects
def manage_permission
raise NotImplementedError
end
def with_unchanged_id
project_id = model.id
model.id = model.id_was
yield
ensure
model.id = project_id
end
end
end

@ -27,7 +27,6 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
module OAuth
##
# Base controller for doorkeeper to skip the login check

@ -0,0 +1,80 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-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 doc/COPYRIGHT.rdoc for more details.
#++
module API
class ParseResourceParamsService
attr_accessor :model,
:representer,
:current_user
def initialize(user, model: nil, representer: nil)
self.current_user = user
self.model = model
self.representer = if !representer && model
deduce_representer(model)
elsif representer
representer
else
raise 'Representer not defined'
end
end
def call(request_body)
parsed = if request_body
parse_attributes(request_body)
else
{}
end
ServiceResult.new(success: true,
result: parsed)
end
private
def deduce_representer(_model)
raise NotImplementedError
end
def parsing_representer
representer
.new(struct, current_user: current_user)
end
def parse_attributes(request_body)
parsing_representer
.from_hash(request_body)
.to_h
end
def struct
OpenStruct.new
end
end
end

@ -28,42 +28,20 @@
module API
module V3
class ParseResourceParamsService
attr_accessor :model,
:representer,
:current_user
def initialize(user, model: nil, representer: nil)
self.current_user = user
self.model = model
class ParseResourceParamsService < ::API::ParseResourceParamsService
private
self.representer = if !representer && model
"API::V3::#{model.to_s.pluralize}::#{model}Representer".constantize
elsif representer
representer
else
raise 'Representer not defined'
end
def deduce_representer(model)
"API::V3::#{model.to_s.pluralize}::#{model}PayloadRepresenter".constantize
end
def call(request_body)
parsed = if request_body
parse_attributes(request_body)
else
{}
end
ServiceResult.new(success: true,
result: parsed)
def parsing_representer
representer
.create(struct, current_user: current_user)
end
private
def parse_attributes(request_body)
representer
.create(struct, current_user: current_user)
.from_hash(request_body)
.to_h
super
.except(:available_custom_fields)
end
@ -71,7 +49,7 @@ module API
if model&.respond_to?(:available_custom_fields)
OpenStruct.new available_custom_fields: model.available_custom_fields(model.new)
else
OpenStruct.new
super
end
end
end

@ -44,6 +44,26 @@ See docs/COPYRIGHT.rdoc for more details.
<code><%= ::Doorkeeper.configuration.native_redirect_uri %></code>
</span>
</div>
<% if Doorkeeper.configuration.scopes.all.length > 1 %>
<div class="form--field">
<label class="form--label">Scopes:</label>
<% Doorkeeper.configuration.scopes.each do |scope| %>
<div class="form--field-container -vertical">
<label class="form--label-with-check-box">
<div class="form--check-box-container">
<%= styled_check_box_tag 'application[scopes][]', scope, f.object.scopes.include?(scope) %>
</div>
<%= scope %>
</label>
</div>
<% end %>
<span class="form--field-instructions">
<%= t('oauth.application.instructions.scopes') %>
</span>
</div>
<% end %>
<div class="form--field">
<%= f.check_box :confidential %>
<span class="form--field-instructions">

@ -2710,6 +2710,7 @@ en:
<br/>
If you're registering a desktop application, use the following URL.
confidential: "Check if the application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are assumed non-confidential."
scopes: "Check the scopes you want the application to grant access to. If no scope is checked, api_v3 is assumed."
client_credential_user_id: "Optional user ID to impersonate when clients use this application. Leave empty to allow public access only"
register_intro: "If you are developing an OAuth API client application for OpenProject, you can register it using this form for all users to use."
default_scopes: ""
@ -2727,8 +2728,8 @@ en:
<br/>
<strong>It has requested the following permissions:</strong>
scopes:
api_v3: "Full API access"
api_v3_text: "Application will receive full read & write access to the OpenProject API to perform actions on your behalf."
api_v3: "Full API v3 access"
api_v3_text: "Application will receive full read & write access to the OpenProject API v3 to perform actions on your behalf."
grants:
created_date: "Approved on"
scopes: "Permissions"

@ -91,7 +91,7 @@ OpenProject::Application.routes.draw do
# returned for all routes for which the v3 has also resources. Grape does
# remove the prefix (v3) before checking whether the method is supported. I
# don't understand why that should make sense.
mount API::Root => '/'
mount API::Root => '/api'
# OAuth authorization routes
use_doorkeeper do

@ -0,0 +1,43 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
class Formatter
def call(object, _env)
if object.is_a?(String)
object
elsif object.respond_to?(:to_json)
object.to_json
else
MultiJson.dump(object)
end
end
end
end

@ -28,203 +28,18 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
# Root class of the API
# This is the place for all API wide configuration, helper methods, exceptions
# rescuing, mounting of differnet API versions etc.
require 'open_project/authentication'
module API
class Root < Grape::API
include OpenProject::Authentication::Scope
extend API::Utilities::GrapeHelper
prefix :api
class Formatter
def call(object, _env)
if object.is_a?(String)
object
elsif object.respond_to?(:to_json)
object.to_json
else
MultiJson.dump(object)
end
end
end
class Parser
def call(object, _env)
MultiJson.load(object)
rescue MultiJson::ParseError => e
error = ::API::Errors::ParseError.new(details: e.message)
representer = ::API::V3::Errors::ErrorRepresenter.new(error)
throw :error, status: 400, message: representer.to_json
end
end
class Root < ::API::RootAPI
content_type 'hal+json', 'application/hal+json; charset=utf-8'
content_type :json, 'application/json; charset=utf-8'
format 'hal+json'
formatter 'hal+json', Formatter.new
parser :json, Parser.new
use OpenProject::Authentication::Manager
helpers API::Caching::Helpers
helpers do
def current_user
User.current
end
def warden
env['warden']
end
##
# Helper to access only the declared
# params to avoid unvalidated access
# (e.g., in before blocks)
def declared_params
declared(params)
end
def request_body
env['api.request.body']
end
def authenticate
warden.authenticate! scope: API_V3
User.current = warden.user scope: API_V3
if Setting.login_required? and not logged_in?
raise ::API::Errors::Unauthenticated
end
end
def set_localization
SetLocalizationService.new(User.current, env['HTTP_ACCEPT_LANGUAGE']).call
end
# Global helper to set allowed content_types
# This may be overriden when multipart is allowed (file uploads)
def allowed_content_types
%w(application/json application/hal+json)
end
def enforce_content_type
# Content-Type is not present in GET
return if request.get?
# Raise if missing header
content_type = request.content_type
error!('Missing content-type header', 406) unless content_type.present?
# Allow JSON and JSON+HAL per default
# and anything that each endpoint may optionally add to that
if content_type.present?
allowed_content_types.each do |mime|
# Content-Type header looks like this (e.g.,)
# application/json;encoding=utf8
return if content_type.start_with?(mime)
end
end
bad_type = content_type.presence || I18n.t('api_v3.errors.missing_content_type')
message = I18n.t('api_v3.errors.invalid_content_type',
content_type: allowed_content_types.join(" "),
actual: bad_type)
fail ::API::Errors::UnsupportedMediaType, message
end
def logged_in?
# An admin SystemUser is anonymous but still a valid user to be logged in.
current_user && (current_user.admin? || !current_user.anonymous?)
end
formatter 'hal+json', API::Formatter.new
default_format 'hal+json'
def authorize(permission, context: nil, global: false, user: current_user, &block)
auth_service = AuthorizationService.new(permission,
context: context,
global: global,
user: user)
parser :json, API::V3::Parser.new
authorize_by_with_raise auth_service, &block
end
error_representer ::API::V3::Errors::ErrorRepresenter, 'hal+json'
authentication_scope OpenProject::Authentication::Scope::API_V3
def authorize_by_with_raise(callable)
is_authorized = callable.respond_to?(:call) ? callable.call : callable
return true if is_authorized
if block_given?
yield
else
raise API::Errors::Unauthorized
end
false
end
# checks whether the user has
# any of the provided permission in any of the provided
# projects
def authorize_any(permissions, projects: nil, global: false, user: current_user, &block)
raise ArgumentError if projects.nil? && !global
projects = Array(projects)
authorized = permissions.any? do |permission|
if global
authorize(permission, global: true, user: user) do
false
end
else
allowed_projects = Project.allowed_to(user, permission)
!(allowed_projects & projects).empty?
end
end
authorize_by_with_raise(authorized, &block)
end
def authorize_admin
authorize_by_with_raise(current_user.admin? && (current_user.active? || current_user.is_a?(SystemUser)))
end
def authorize_logged_in
authorize_by_with_raise(current_user.logged? && current_user.active? || current_user.is_a?(SystemUser))
end
def raise_invalid_query_on_service_failure
service = yield
if service.success?
service
else
api_errors = service.errors.full_messages.map do |message|
::API::Errors::InvalidQuery.new(message)
end
raise ::API::Errors::MultipleErrors.create_if_many api_errors
end
end
end
def self.auth_headers
lambda do
header = OpenProject::Authentication::WWWAuthenticate
.response_header(scope: API_V3, request_headers: env)
{ 'WWW-Authenticate' => header }
end
end
##
# Return JSON error response on authentication failure.
OpenProject::Authentication.handle_failure(scope: API_V3) do |warden, _opts|
e = grape_error_for warden.env, self
error_message = I18n.t('api_v3.errors.code_401_wrong_credentials')
@ -234,38 +49,6 @@ module API
e.error_response status: 401, message: representer.to_json, headers: warden.headers, log: false
end
error_response ActiveRecord::RecordNotFound, ::API::Errors::NotFound, log: false
error_response ActiveRecord::StaleObjectError, ::API::Errors::Conflict, log: false
error_response MultiJson::ParseError, ::API::Errors::ParseError
error_response ::API::Errors::Unauthenticated, headers: auth_headers, log: false
error_response ::API::Errors::ErrorBase, rescue_subclasses: true, log: false
# Handle grape validation errors
error_response ::Grape::Exceptions::ValidationErrors, ::API::Errors::BadRequest, log: false
# Handle connection timeouts with appropriate payload
error_response ActiveRecord::ConnectionTimeoutError,
::API::Errors::InternalError,
log: ->(exception) do
payload = ::OpenProject::Logging::ThreadPoolContextBuilder.build!
::OpenProject.logger.error exception, reference: :APIv3, payload: payload
end
# hide internal errors behind the same JSON response as all other errors
# only doing it in production to allow for easier debugging
if Rails.env.production?
error_response StandardError, ::API::Errors::InternalError, rescue_subclasses: true
end
# run authentication before each request
after_validation do
authenticate
set_localization
enforce_content_type
end
version 'v3', using: :path do
mount API::V3::Root
end

@ -0,0 +1,244 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
# Root class of the API
# This is the place for all API wide configuration, helper methods, exceptions
# rescuing, mounting of differnet API versions etc.
require 'open_project/authentication'
module API
class RootAPI < Grape::API
include OpenProject::Authentication::Scope
extend API::Utilities::GrapeHelper
content_type :json, 'application/json; charset=utf-8'
use OpenProject::Authentication::Manager
helpers API::Caching::Helpers
helpers do
def current_user
User.current
end
def warden
env['warden']
end
##
# Helper to access only the declared
# params to avoid unvalidated access
# (e.g., in before blocks)
def declared_params
declared(params)
end
def request_body
env['api.request.body']
end
def authenticate
warden.authenticate! scope: authentication_scope
User.current = warden.user scope: authentication_scope
if Setting.login_required? and not logged_in?
raise ::API::Errors::Unauthenticated
end
end
def set_localization
SetLocalizationService.new(User.current, env['HTTP_ACCEPT_LANGUAGE']).call
end
# Global helper to set allowed content_types
# This may be overriden when multipart is allowed (file uploads)
def allowed_content_types
%w(application/json application/hal+json)
end
def enforce_content_type
# Content-Type is not present in GET
return if request.get?
# Raise if missing header
content_type = request.content_type
error!('Missing content-type header', 406) unless content_type.present?
# Allow JSON and JSON+HAL per default
# and anything that each endpoint may optionally add to that
if content_type.present?
allowed_content_types.each do |mime|
# Content-Type header looks like this (e.g.,)
# application/json;encoding=utf8
return if content_type.start_with?(mime)
end
end
bad_type = content_type.presence || I18n.t('api_v3.errors.missing_content_type')
message = I18n.t('api_v3.errors.invalid_content_type',
content_type: allowed_content_types.join(" "),
actual: bad_type)
fail ::API::Errors::UnsupportedMediaType, message
end
def logged_in?
# An admin SystemUser is anonymous but still a valid user to be logged in.
current_user && (current_user.admin? || !current_user.anonymous?)
end
def authorize(permission, context: nil, global: false, user: current_user, &block)
auth_service = AuthorizationService.new(permission,
context: context,
global: global,
user: user)
authorize_by_with_raise auth_service, &block
end
def authorize_by_with_raise(callable)
is_authorized = callable.respond_to?(:call) ? callable.call : callable
return true if is_authorized
if block_given?
yield
else
raise API::Errors::Unauthorized
end
false
end
# checks whether the user has
# any of the provided permission in any of the provided
# projects
def authorize_any(permissions, projects: nil, global: false, user: current_user, &block)
raise ArgumentError if projects.nil? && !global
projects = Array(projects)
authorized = permissions.any? do |permission|
if global
authorize(permission, global: true, user: user) do
false
end
else
allowed_projects = Project.allowed_to(user, permission)
!(allowed_projects & projects).empty?
end
end
authorize_by_with_raise(authorized, &block)
end
def authorize_admin
authorize_by_with_raise(current_user.admin? && (current_user.active? || current_user.is_a?(SystemUser)))
end
def authorize_logged_in
authorize_by_with_raise(current_user.logged? && current_user.active? || current_user.is_a?(SystemUser))
end
def raise_invalid_query_on_service_failure
service = yield
if service.success?
service
else
api_errors = service.errors.full_messages.map do |message|
::API::Errors::InvalidQuery.new(message)
end
raise ::API::Errors::MultipleErrors.create_if_many api_errors
end
end
end
def self.auth_headers
lambda do
header = OpenProject::Authentication::WWWAuthenticate
.response_header(scope: authentication_scope, request_headers: env)
{ 'WWW-Authenticate' => header }
end
end
def self.error_representer(klass, content_type)
# Have the vars available in the instances via helpers.
helpers do
define_method(:error_representer, -> { klass })
define_method(:error_content_type, -> { content_type })
end
end
def self.authentication_scope(sym)
# Have the scope available in the instances
# via a helper.
helpers do
define_method(:authentication_scope, -> { sym })
end
end
error_response ActiveRecord::RecordNotFound, ::API::Errors::NotFound, log: false
error_response ActiveRecord::StaleObjectError, ::API::Errors::Conflict, log: false
error_response MultiJson::ParseError, ::API::Errors::ParseError
error_response ::API::Errors::Unauthenticated, headers: auth_headers, log: false
error_response ::API::Errors::ErrorBase, rescue_subclasses: true, log: false
# Handle grape validation errors
error_response ::Grape::Exceptions::ValidationErrors, ::API::Errors::BadRequest, log: false
# Handle connection timeouts with appropriate payload
error_response ActiveRecord::ConnectionTimeoutError,
::API::Errors::InternalError,
log: ->(exception) do
payload = ::OpenProject::Logging::ThreadPoolContextBuilder.build!
::OpenProject.logger.error exception, reference: :APIv3, payload: payload
end
# hide internal errors behind the same JSON response as all other errors
# only doing it in production to allow for easier debugging
if Rails.env.production?
error_response StandardError, ::API::Errors::InternalError, rescue_subclasses: true
end
# run authentication before each request
after_validation do
authenticate
set_localization
enforce_content_type
end
end
end

@ -0,0 +1,172 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module Utilities
module Endpoints
class Bodied
def default_instance_generator(_model)
raise NotImplementedError
end
def default_params_modifier
->(params) do
params
end
end
def initialize(model:,
api_name: model.name.demodulize,
instance_generator: default_instance_generator(model),
params_modifier: default_params_modifier,
process_service: nil,
parse_service: nil)
self.model = model
self.api_name = api_name
self.instance_generator = instance_generator
self.params_modifier = params_modifier
self.parse_representer = deduce_parse_representer
self.render_representer = deduce_render_representer
self.process_contract = deduce_process_contract
self.process_service = process_service || deduce_process_service
self.parse_service = parse_service || deduce_parse_service
end
def mount
update = self
-> do
params = update.parse(current_user, request_body)
params = instance_exec(params, &update.params_modifier)
call = update.process(current_user,
instance_exec(params, &update.instance_generator),
params)
update.render(current_user, call) do
status update.success_status
end
end
end
def parse(current_user, request_body)
parse_service
.new(current_user,
model: model,
representer: parse_representer)
.call(request_body)
.result
end
def process(current_user, instance, params)
args = { user: current_user,
model: instance,
contract_class: process_contract }
process_service
.new(args.compact)
.call(params)
end
def render(current_user, call)
if success?(call)
yield
present_success(current_user, call)
else
present_error(call)
end
end
def success_status
:ok
end
attr_accessor :model,
:api_name,
:instance_generator,
:parse_representer,
:render_representer,
:params_modifier,
:process_contract,
:process_service,
:parse_service
private
def present_success(_current_user, _call)
raise NotImplementedError
end
def present_error(_call)
raise NotImplementedError
end
def success?(call)
call.success?
end
def deduce_process_service
"::#{deduce_backend_namespace}::SetAttributesService".constantize
end
def deduce_process_contract
"::#{deduce_backend_namespace}::#{update_or_create}Contract".constantize
end
def deduce_parse_representer
raise NotImplementedError
end
def deduce_parse_service
raise NotImplementedError
end
def deduce_render_representer
raise NotImplementedError
end
def deduce_api_namespace
api_name.pluralize
end
def backend_name
model.name.demodulize
end
def deduce_backend_namespace
backend_name.pluralize
end
def update_or_create
raise NotImplementedError
end
end
end
end
end

@ -0,0 +1,50 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module Utilities
module Endpoints
class Create < Modify
def default_instance_generator(_model)
->(_params) do
end
end
def success_status
:created
end
private
def update_or_create
"Create"
end
end
end
end
end

@ -0,0 +1,107 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module Utilities
module Endpoints
class Delete
def default_instance_generator(model)
->(_params) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
def initialize(model:,
instance_generator: default_instance_generator(model),
process_service: nil)
self.model = model
self.instance_generator = instance_generator
self.process_service = process_service || deduce_process_service
end
def mount
delete = self
-> do
call = delete.process(current_user,
instance_exec(params, &delete.instance_generator))
delete.render(call) do
status delete.success_status
end
end
end
def process(current_user, instance)
process_service
.new(user: current_user,
model: instance)
.call
end
def render(call)
if success?(call)
yield
else
present_error(call)
end
end
def success_status
204
end
attr_accessor :model,
:instance_generator,
:process_service
private
def present_error(call)
fail ::API::Errors::ErrorBase.create_and_merge_errors(call.errors)
end
def success?(call)
call.success?
end
def deduce_process_service
"::#{deduce_namespace}::DeleteService".constantize
end
def deduce_namespace
demodulized_name.pluralize
end
def demodulized_name
model.name.demodulize
end
end
end
end
end

@ -0,0 +1,74 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module Utilities
module Endpoints
class Index
include ::API::Utilities::PageSizeHelper
def initialize(model:,
api_name: model.name.demodulize,
scope: nil,
render_representer: nil)
self.model = model_class(model)
self.scope = scope
self.api_name = api_name
self.render_representer = render_representer || deduce_render_representer
end
def mount
raise NotImplementedError
end
attr_accessor :model,
:api_name,
:scope,
:render_representer
private
def deduce_render_representer
raise NotImplementedError
end
def deduce_api_namespace
api_name.pluralize
end
def model_class(scope)
if scope.is_a? Class
scope
else
scope.model
end
end
end
end
end
end

@ -0,0 +1,88 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module Utilities
module Endpoints
class Modify < Bodied
def default_instance_generator(model)
->(_params, _current_user) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
private
def present_success(_current_user, _call)
raise NotImplementedError
end
def present_error(call)
errors = call.errors
errors = merge_dependent_errors call if errors.empty?
api_errors = [::API::Errors::ErrorBase.create_and_merge_errors(errors)]
fail ::API::Errors::MultipleErrors.create_if_many(api_errors)
end
def merge_dependent_errors(call)
errors = ActiveModel::Errors.new call.result
call.dependent_results.each do |dr|
dr.errors.keys.each do |field|
dr.errors.symbols_and_messages_for(field).each do |symbol, full_message, _|
errors.add :base, symbol, message: dependent_error_message(dr.result, full_message)
end
end
end
errors
end
def dependent_error_message(result, full_message)
I18n.t(
:error_in_dependent,
dependent_class: result.model_name.human,
related_id: result.id,
related_subject: result.name,
error: full_message
)
end
def deduce_process_service
"::#{deduce_backend_namespace}::#{update_or_create}Service".constantize
end
def deduce_process_contract
nil
end
end
end
end
end

@ -0,0 +1,86 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module Utilities
module Endpoints
class Show
def default_instance_generator(model)
->(_params) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
def initialize(model:,
api_name: model.name.demodulize,
render_representer: nil,
instance_generator: default_instance_generator(model))
self.model = model
self.api_name = api_name
self.instance_generator = instance_generator
self.render_representer = render_representer || deduce_render_representer
end
def mount
show = self
-> do
show.render(instance_exec(params, &show.instance_generator))
end
end
def render(instance)
render_representer
.create(instance,
current_user: User.current,
embed_links: true)
end
def self_path
api_name.underscore.pluralize
end
attr_accessor :model,
:api_name,
:instance_generator,
:render_representer
private
def deduce_render_representer
raise NotImplementedError
end
def deduce_api_namespace
api_name.pluralize
end
end
end
end
end

@ -0,0 +1,47 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module Utilities
module Endpoints
class Update < Modify
def default_instance_generator(model)
->(_params) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
private
def update_or_create
"Update"
end
end
end
end
end

@ -49,7 +49,7 @@ module API
end
end
def error_response(rescued_error, error = nil, rescue_subclasses: nil, headers: ->() { {} }, log: true)
def error_response(rescued_error, error = nil, rescue_subclasses: nil, headers: -> { {} }, log: true)
error_response_lambda = default_error_response(headers, log)
response =
@ -67,9 +67,9 @@ module API
def default_error_response(headers, log)
lambda { |e|
original_exception = $!
representer = ::API::V3::Errors::ErrorRepresenter.new e
representer = error_representer.new e
resp_headers = instance_exec &headers
env['api.format'] = 'hal+json'
env['api.format'] = error_content_type
if log == true
OpenProject.logger.error original_exception, reference: :APIv3

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
@ -41,14 +42,14 @@ module API
property :_type, exec_context: :decorator
property :error_identifier, exec_context: :decorator, render_nil: true
property :message, getter: -> (*) { message }, render_nil: true
property :message, getter: ->(*) { message }, render_nil: true
property :details, embedded: true
collection :errors,
embedded: true,
class: ::API::Errors::ErrorBase,
decorator: ::API::V3::Errors::ErrorRepresenter,
if: -> (*) { !Array(errors).empty? }
if: ->(*) { !Array(errors).empty? }
def _type
'Error'

@ -0,0 +1,42 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API::V3
class Parser
def call(object, _env)
MultiJson.load(object)
rescue MultiJson::ParseError => e
error = ::API::Errors::ParseError.new(details: e.message)
representer = ::API::V3::Errors::ErrorRepresenter.new(error)
throw :error, status: 400, message: representer.to_json
end
end
end

@ -52,11 +52,7 @@ module API
end
route_param :id do
after_validation do
@project = Project.find(params[:id])
authorize(:view_project, context: @project) do
raise API::Errors::NotFound.new
end
@project = Project.visible(current_user).find(params[:id])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: Project).mount

@ -1,170 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module V3
module Utilities
module Endpoints
class Bodied
def default_instance_generator(_model)
raise NotImplementedError
end
def default_params_modifier
->(params) do
params
end
end
def initialize(model:,
api_name: model.name.demodulize,
instance_generator: default_instance_generator(model),
params_modifier: default_params_modifier,
process_service: nil,
parse_service: API::V3::ParseResourceParamsService)
self.model = model
self.api_name = api_name
self.instance_generator = instance_generator
self.params_modifier = params_modifier
self.parse_representer = deduce_parse_representer
self.render_representer = deduce_render_representer
self.process_contract = deduce_process_contract
self.process_service = process_service || deduce_process_service
self.parse_service = parse_service
end
def mount
update = self
-> do
params = update.parse(current_user, request_body)
params = instance_exec(params, &update.params_modifier)
call = update.process(current_user,
instance_exec(params, &update.instance_generator),
params)
update.render(current_user, call) do
status update.success_status
end
end
end
def parse(current_user, request_body)
parse_service
.new(current_user,
model: model,
representer: parse_representer)
.call(request_body)
.result
end
def process(current_user, instance, params)
args = { user: current_user,
model: instance,
contract_class: process_contract }
process_service
.new(args.compact)
.call(params)
end
def render(current_user, call)
if success?(call)
yield
present_success(current_user, call)
else
present_error(call)
end
end
def success_status
:ok
end
attr_accessor :model,
:api_name,
:instance_generator,
:parse_representer,
:render_representer,
:params_modifier,
:process_contract,
:process_service,
:parse_service
private
def present_success(_current_user, _call)
raise NotImplementedError
end
def present_error(_call)
raise NotImplementedError
end
def success?(call)
call.success?
end
def deduce_process_service
"::#{decude_backend_namespace}::SetAttributesService".constantize
end
def deduce_process_contract
"::#{decude_backend_namespace}::#{update_or_create}Contract".constantize
end
def deduce_parse_representer
"::API::V3::#{deduce_api_namespace}::#{api_name}PayloadRepresenter".constantize
end
def deduce_render_representer
raise NotImplementedError
end
def deduce_api_namespace
api_name.pluralize
end
def backend_name
model.name.demodulize
end
def decude_backend_namespace
backend_name.pluralize
end
def update_or_create
raise NotImplementedError
end
end
end
end
end
end

@ -30,21 +30,9 @@ module API
module V3
module Utilities
module Endpoints
class Create < Modify
def default_instance_generator(_model)
->(_params) do
end
end
def success_status
:created
end
private
def update_or_create
"Create"
end
class Create < API::Utilities::Endpoints::Create
include V3Deductions
include V3PresentSingle
end
end
end

@ -30,78 +30,7 @@ module API
module V3
module Utilities
module Endpoints
class Delete
def default_instance_generator(model)
->(_params) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
def initialize(model:,
instance_generator: default_instance_generator(model),
process_service: nil)
self.model = model
self.instance_generator = instance_generator
self.process_service = process_service || deduce_process_service
end
def mount
delete = self
-> do
call = delete.process(current_user,
instance_exec(params, &delete.instance_generator))
delete.render(call) do
status delete.success_status
end
end
end
def process(current_user, instance)
process_service
.new(user: current_user,
model: instance)
.call
end
def render(call)
if success?(call)
yield
else
present_error(call)
end
end
def success_status
204
end
attr_accessor :model,
:instance_generator,
:process_service
private
def present_error(call)
fail ::API::Errors::ErrorBase.create_and_merge_errors(call.errors)
end
def success?(call)
call.success?
end
def deduce_process_service
"::#{deduce_namespace}::DeleteService".constantize
end
def deduce_namespace
demodulized_name.pluralize
end
def demodulized_name
model.name.demodulize
end
class Delete < API::Utilities::Endpoints::Delete
end
end
end

@ -30,7 +30,9 @@ module API
module V3
module Utilities
module Endpoints
class Form < Bodied
class Form < API::Utilities::Endpoints::Bodied
include V3Deductions
def success?(call)
only_validation_errors?(api_errors(call))
end

@ -30,19 +30,9 @@ module API
module V3
module Utilities
module Endpoints
class Index
class Index < API::Utilities::Endpoints::Index
include ::API::Utilities::PageSizeHelper
def initialize(model:,
api_name: model.name.demodulize,
scope: nil,
render_representer: nil)
self.model = model_class(model)
self.scope = scope
self.api_name = api_name
self.render_representer = render_representer || deduce_render_representer
end
def mount
index = self
@ -116,10 +106,10 @@ module API
end
def deduce_render_representer
"::API::V3::#{deduce_namespace}::#{api_name}CollectionRepresenter".constantize
"::API::V3::#{deduce_api_namespace}::#{api_name}CollectionRepresenter".constantize
end
def deduce_namespace
def deduce_api_namespace
api_name.pluralize
end
@ -131,14 +121,6 @@ module API
end
end
def with_based_scope(other_scope)
if scope
merge_scopes(scope, other_scope)
else
other_scope
end
end
def merge_scopes(scope_a, scope_b)
if scope_a.is_a? Class
scope_b

@ -1,97 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module V3
module Utilities
module Endpoints
class Modify < Bodied
def default_instance_generator(model)
->(_params, _current_user) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
private
def present_success(current_user, call)
render_representer
.create(call.result,
current_user: current_user,
embed_links: true)
end
def present_error(call)
errors = call.errors
errors = merge_dependent_errors call if errors.empty?
api_errors = [::API::Errors::ErrorBase.create_and_merge_errors(errors)]
fail ::API::Errors::MultipleErrors.create_if_many(api_errors)
end
def merge_dependent_errors(call)
errors = ActiveModel::Errors.new call.result
call.dependent_results.each do |dr|
dr.errors.keys.each do |field|
dr.errors.symbols_and_messages_for(field).each do |symbol, full_message, _|
errors.add :base, symbol, message: dependent_error_message(dr.result, full_message)
end
end
end
errors
end
def dependent_error_message(result, full_message)
I18n.t(
:error_in_dependent,
dependent_class: result.model_name.human,
related_id: result.id,
related_subject: result.name,
error: full_message
)
end
def deduce_process_service
"::#{decude_backend_namespace}::#{update_or_create}Service".constantize
end
def deduce_render_representer
"::API::V3::#{deduce_api_namespace}::#{api_name}Representer".constantize
end
def deduce_process_contract
nil
end
end
end
end
end
end

@ -30,57 +30,8 @@ module API
module V3
module Utilities
module Endpoints
class Show
def default_instance_generator(model)
->(_params) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
def initialize(model:,
api_name: model.name.demodulize,
render_representer: nil,
instance_generator: default_instance_generator(model))
self.model = model
self.api_name = api_name
self.instance_generator = instance_generator
self.render_representer = render_representer || deduce_render_representer
end
def mount
show = self
-> do
show.render(instance_exec(params, &show.instance_generator))
end
end
def render(instance)
render_representer
.create(instance,
current_user: User.current,
embed_links: true)
end
def self_path
api_name.underscore.pluralize
end
attr_accessor :model,
:api_name,
:instance_generator,
:render_representer
private
def deduce_render_representer
"::API::V3::#{deduce_namespace}::#{api_name}Representer".constantize
end
def deduce_namespace
api_name.pluralize
end
class Show < API::Utilities::Endpoints::Show
include V3Deductions
end
end
end

@ -30,18 +30,9 @@ module API
module V3
module Utilities
module Endpoints
class Update < Modify
def default_instance_generator(model)
->(_params) do
instance_variable_get("@#{model.name.demodulize.underscore}")
end
end
private
def update_or_create
"Update"
end
class Update < API::Utilities::Endpoints::Update
include V3Deductions
include V3PresentSingle
end
end
end

@ -0,0 +1,51 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module V3
module Utilities
module Endpoints
module V3Deductions
private
def deduce_parse_service
API::V3::ParseResourceParamsService
end
def deduce_render_representer
"::API::V3::#{deduce_api_namespace}::#{api_name}Representer".constantize
end
def deduce_parse_representer
"::API::V3::#{deduce_api_namespace}::#{api_name}PayloadRepresenter".constantize
end
end
end
end
end
end

@ -0,0 +1,44 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module API
module V3
module Utilities
module Endpoints
module V3PresentSingle
def present_success(current_user, call)
render_representer
.create(call.result,
current_user: current_user,
embed_links: true)
end
end
end
end
end
end

@ -58,8 +58,7 @@ module OpenProject
end
def self.host
host = Setting.host_name
host.gsub(/\/.*$/, '') if host # remove path in case it got into the host
Setting.host_name&.gsub(/\/.*$/, '') # remove path in case it got into the host
end
end
@ -82,7 +81,7 @@ module OpenProject
return nil unless path.present?
# Remove relative URL root
if relative_url = OpenProject::Configuration.rails_relative_url_root
if (relative_url = OpenProject::Configuration.rails_relative_url_root)
path = path.gsub relative_url, ''
end

@ -0,0 +1,56 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
# Root class of the API
# This is the place for all API wide configuration, helper methods, exceptions
# rescuing, mounting of different API versions etc.
module Bcf::API
class Root < ::API::RootAPI
format :json
formatter :json, API::Formatter.new
default_format :json
error_representer ::Bcf::API::V2_1::Errors::ErrorRepresenter, :json
error_formatter :json, ::Bcf::API::ErrorFormatter::Json
authentication_scope OpenProject::Authentication::Scope::BCF_V2_1
version '2.1', using: :path do
# /auth
mount ::Bcf::API::V2_1::AuthAPI
# /current-user
mount ::Bcf::API::V2_1::CurrentUserAPI
# /projects
mount ::Bcf::API::V2_1::ProjectsAPI
end
end
end

@ -0,0 +1,39 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1
class AuthAPI < ::API::OpenProjectAPI
resources :auth do
get do
::Bcf::API::V2_1::Auth::SingleRepresenter.new(nil)
end
end
end
end

@ -0,0 +1,38 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1
class CurrentUserAPI < ::API::OpenProjectAPI
resources :'current-user' do
get &::Bcf::API::V2_1::Endpoints::Show.new(model: User,
instance_generator: ->(*) { current_user }).mount
end
end
end

@ -0,0 +1,57 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1::Endpoints
class Index < API::Utilities::Endpoints::Index
def mount
index = self
-> do
collection = index.scope ? instance_exec(&index.scope) : index.model
index.render(collection)
end
end
def render(collection)
collection
.map do |instance|
render_representer
.new(instance)
end
end
private
def deduce_render_representer
"::Bcf::API::V2_1::#{deduce_api_namespace}::SingleRepresenter".constantize
end
end
end

@ -0,0 +1,44 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1::Endpoints
class Show < API::Utilities::Endpoints::Show
def render(instance)
render_representer
.new(instance)
end
private
def deduce_render_representer
"::Bcf::API::V2_1::#{deduce_api_namespace}::SingleRepresenter".constantize
end
end
end

@ -0,0 +1,51 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1::Endpoints
class Update < API::Utilities::Endpoints::Update
def present_success(_current_user, call)
render_representer
.new(call.result)
end
private
def deduce_parse_service
Bcf::API::V2_1::ParseResourceParamsService
end
def deduce_in_and_out_representer
"::Bcf::API::V2_1::#{deduce_api_namespace}::SingleRepresenter".constantize
end
alias_method :deduce_parse_representer, :deduce_in_and_out_representer
alias_method :deduce_render_representer, :deduce_in_and_out_representer
end
end

@ -0,0 +1,57 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1
class ProjectsAPI < ::API::OpenProjectAPI
resources :projects do
helpers do
def visible_projects
Project
.visible(current_user)
.has_module(:bcf)
end
end
get &::Bcf::API::V2_1::Endpoints::Index.new(model: Project,
scope: -> { visible_projects })
.mount
route_param :id, regexp: /\A(\d+)\z/ do
after_validation do
@project = visible_projects
.find(params[:id])
end
get &::Bcf::API::V2_1::Endpoints::Show.new(model: Project).mount
put &::Bcf::API::V2_1::Endpoints::Update.new(model: Project).mount
end
end
end
end

@ -0,0 +1,43 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API
module ErrorFormatter
module Json
extend Grape::ErrorFormatter::Base
class << self
def call(message, _backtrace, _options = {}, env = nil, _original_exception = nil)
present(message, env)
end
end
end
end
end

@ -0,0 +1,56 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1
class Auth::SingleRepresenter < Roar::Decorator
include Representable::JSON
include OpenProject::StaticRouting::UrlHelpers
property :oauth2_auth_url,
getter: ->(decorator:, **) {
"#{decorator.root_url}oauth/authorize"
}
property :oauth2_token_url,
getter: ->(decorator:, **) {
"#{decorator.root_url}oauth/token"
}
property :supported_oauth2_flows,
getter: ->(*) {
%w(authorization_code_grant client_credentials)
}
property :http_basic_supported,
getter: ->(*) {
false
}
end
end

@ -0,0 +1,39 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1::Errors
class ErrorRepresenter < Roar::Decorator
include Representable::JSON
property :message,
getter: ->(*) { message },
render_nil: true
end
end

@ -0,0 +1,40 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1
class Projects::SingleRepresenter < Roar::Decorator
include Representable::JSON
property :id,
as: :project_id
property :name
end
end

@ -0,0 +1,40 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Bcf::API::V2_1
class Users::SingleRepresenter < Roar::Decorator
include Representable::JSON
property :mail,
as: :id
property :name
end
end

@ -0,0 +1,46 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-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 doc/COPYRIGHT.rdoc for more details.
#++
module Bcf
module API
module V2_1
class ParseResourceParamsService < ::API::ParseResourceParamsService
private
def deduce_representer(model)
"Bcf::API::V2_1::#{model.to_s.pluralize}::SingleRepresenter".constantize
end
def parsing_representer
representer
.new(struct)
end
end
end
end
end

@ -67,3 +67,8 @@ en:
project_module_bcf: "BCF"
permission_view_linked_issues: "View BCF issues"
permission_manage_bcf: "Import and manage BCF issues"
oauth:
scopes:
bcf_v2_1: "Full access to the BCF v2.1 API"
bcf_v2_1_text: "Application will receive full read & write access to the OpenProject BCF API v2.1 to perform actions on your behalf."

@ -35,6 +35,8 @@
OpenProject::Application.routes.draw do
scope '', as: 'bcf' do
mount Bcf::API::Root => '/api/bcf'
scope 'projects/:project_id', as: 'project' do
resources :issues, controller: 'bcf/issues' do
get :upload, action: :upload, on: :collection

@ -42,7 +42,6 @@ module OpenProject::Bcf
default: {
}
} do
project_module :bcf do
permission :view_linked_issues,
{ 'bcf/issues': %i[index] },
@ -146,6 +145,19 @@ module OpenProject::Bcf
Mime::Type.register "application/octet-stream", :bcfzip unless Mime::Type.lookup_by_extension(:bcfzip)
end
initializer 'bcf.add_api_scope' do
Doorkeeper.configuration.scopes.add(:bcf_v2_1)
module OpenProject::Authentication::Scope
BCF_V2_1 = :bcf_v2_1
end
OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::BCF_V2_1,
store: false) do |_strategies|
%i[oauth session]
end
end
config.to_prepare do
::WorkPackage::Exporter
.register_for_list(:bcf, OpenProject::Bcf::BcfXml::Exporter)

@ -0,0 +1,148 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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'
describe 'authorization for BCF api', type: :feature, js: true do
let!(:user) { FactoryBot.create(:admin) }
let(:client_secret) { app.plaintext_secret }
let(:scope) { 'bcf_v2_1' }
let!(:project) { FactoryBot.create(:project) }
def oauth_path(client_id)
"/oauth/authorize?response_type=code&client_id=#{client_id}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=#{scope}"
end
before do
login_with user.login, 'adminADMIN!'
visit oauth_applications_path
end
it 'can create and later authorize and manage an OAuth application grant and then use the access token for the bcf api' do
# Initially empty
expect(page).to have_selector('.generic-table--empty-row', text: 'There is currently nothing to display')
# Create application
find('.button', text: 'Add').click
fill_in 'application_name', with: 'My API application'
# Limit to bcf access
check scope
# Fill invalid redirect_uri
fill_in 'application_redirect_uri', with: "not a url!"
click_on 'Create'
expect(page).to have_selector('.errorExplanation', text: 'Redirect URI must be an absolute URI.')
fill_in 'application_redirect_uri', with: "urn:ietf:wg:oauth:2.0:oob\nhttps://localhost/my/callback"
click_on 'Create'
expect(page).to have_selector('.flash.notice', text: 'Successful creation.')
expect(page).to have_selector('.attributes-key-value--key',
text: 'Client ID')
expect(page).to have_selector('.attributes-key-value--value',
text: "urn:ietf:wg:oauth:2.0:oob\nhttps://localhost/my/callback")
# Should print secret on initial visit
expect(page).to have_selector('.attributes-key-value--key', text: 'Client secret')
client_secret = page.first('.attributes-key-value--value code').text
expect(client_secret).to match /\w+/
app = ::Doorkeeper::Application.first
visit oauth_path app.uid
# We get to the authorization screen
expect(page).to have_selector('h2', text: 'Authorize My API application')
# With the correct scope printed
expect(page).to have_selector('li strong', text: I18n.t('oauth.scopes.bcf_v2_1'))
expect(page).to have_selector('li', text: I18n.t('oauth.scopes.bcf_v2_1_text'))
# Authorize
find('input.button[value="Authorize"]').click
# Expect auth token
code = find('#authorization_code').text
# And also have a grant for this application
user.oauth_grants.reload
expect(user.oauth_grants.count).to eq 1
expect(user.oauth_grants.first.application).to eq app
parameters = {
client_id: app.uid,
client_secret: client_secret,
code: code,
grant_type: :authorization_code,
redirect_uri: app.redirect_uri.split.first
}
session = ActionDispatch::Integration::Session.new(Rails.application)
response = session.post("/oauth/token", params: parameters)
expect(response).to eq 200
body = JSON.parse(session.response.body)
expect(body['access_token']).to be_present
expect(body['refresh_token']).to be_present
expect(body['scope']).to eq scope
access_token = body['access_token']
# Should show that grant in my account
visit my_account_path
click_on 'Access token'
expect(page).to have_selector("#oauth-application-grant-#{app.id}", text: app.name)
expect(page).to have_selector('td', text: app.name)
# While being logged in, the api can be accessed with the session
visit("/api/bcf/2.1/projects/#{project.id}")
expect(page)
.to have_content(JSON.dump(project_id: project.id, name: project.name))
logout
# While not being logged in and without a token, the api cannot be accessed
visit("/api/bcf/2.1/projects/#{project.id}")
expect(page)
.to have_content(JSON.dump(message: "The requested resource could not be found."))
## Without the access token, access is denied
api_session = ActionDispatch::Integration::Session.new(Rails.application)
response = api_session.get("/api/bcf/2.1/projects/#{project.id}")
expect(response).to eq 404
# With the access token, access is allowed
response = api_session.get("/api/bcf/2.1/projects/#{project.id}",
headers: { 'Authorization': "Bearer #{access_token}" })
expect(response).to eq 200
expect(api_session.response.body)
.to be_json_eql({ project_id: project.id, name: project.name }.to_json)
end
end

@ -0,0 +1,74 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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-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_relative '../shared_examples'
describe Bcf::API::V2_1::Auth::SingleRepresenter, 'rendering' do
let(:instance) { described_class.new(nil) }
include OpenProject::StaticRouting::UrlHelpers
subject { instance.to_json }
describe 'attributes' do
before do
allow(OpenProject::Configuration)
.to receive(:rails_relative_url_root)
.and_return('/blubs')
end
context 'oauth2_auth_url' do
it_behaves_like 'attribute' do
let(:value) { "http://localhost:3000/blubs/oauth/authorize" }
let(:path) { 'oauth2_auth_url' }
end
end
context 'oauth2_token_url' do
it_behaves_like 'attribute' do
let(:value) { "http://localhost:3000/blubs/oauth/token" }
let(:path) { 'oauth2_token_url' }
end
end
context 'http_basic_supported' do
it_behaves_like 'attribute' do
let(:value) { false }
let(:path) { 'http_basic_supported' }
end
end
context 'supported_oauth2_flows' do
it_behaves_like 'attribute' do
let(:value) { %w(authorization_code_grant client_credentials) }
let(:path) { 'supported_oauth2_flows' }
end
end
end
end

@ -0,0 +1,55 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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-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_relative '../shared_examples'
describe Bcf::API::V2_1::Projects::SingleRepresenter, 'rendering' do
let(:project) { FactoryBot.build_stubbed(:project) }
let(:instance) { described_class.new(project) }
subject { instance.to_json }
describe 'attributes' do
context 'project_id' do
it_behaves_like 'attribute' do
let(:value) { project.id }
let(:path) { 'project_id' }
end
end
context 'name' do
it_behaves_like 'attribute' do
let(:value) { project.name }
let(:path) { 'name' }
end
end
end
end

@ -0,0 +1,8 @@
shared_examples_for 'attribute' do
it 'reflects the project' do
expect(subject)
.to be_json_eql(value.to_json)
.at_path(path)
end
end

@ -0,0 +1,55 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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-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_relative '../shared_examples'
describe Bcf::API::V2_1::Users::SingleRepresenter, 'rendering' do
let(:user) { FactoryBot.build_stubbed(:user) }
let(:instance) { described_class.new(user) }
subject { instance.to_json }
describe 'attributes' do
context 'id' do
it_behaves_like 'attribute' do
let(:value) { user.mail }
let(:path) { 'id' }
end
end
context 'name' do
it_behaves_like 'attribute' do
let(:value) { user.name }
let(:path) { 'name' }
end
end
end
end

@ -0,0 +1,62 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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-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'
require_relative './shared_responses'
describe 'BCF 2.1 auth resource', type: :request, content_type: :json do
include Rack::Test::Methods
let(:current_user) do
FactoryBot.create(:user)
end
subject(:response) { last_response }
describe 'GET /api/bcf/2.1/auth' do
let(:path) { "/api/bcf/2.1/auth" }
before do
login_as(current_user)
get path
end
it_behaves_like 'bcf api successful response' do
let(:expected_body) do
{
"oauth2_auth_url": "http://localhost:3000/oauth/authorize",
"oauth2_token_url": "http://localhost:3000/oauth/token",
"http_basic_supported": false,
"supported_oauth2_flows": %w(authorization_code_grant client_credentials)
}
end
end
end
end

@ -0,0 +1,60 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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-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'
require_relative './shared_responses'
describe 'BCF 2.1 current-user resource', type: :request, content_type: :json do
include Rack::Test::Methods
let(:current_user) do
FactoryBot.create(:user)
end
subject(:response) { last_response }
describe 'GET /api/bcf/2.1/current-user' do
let(:path) { "/api/bcf/2.1/current-user" }
before do
login_as(current_user)
get path
end
it_behaves_like 'bcf api successful response' do
let(:expected_body) do
{
id: current_user.mail,
name: current_user.name
}
end
end
end
end

@ -0,0 +1,156 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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-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'
require_relative './shared_responses'
describe 'BCF 2.1 projects resource', type: :request, content_type: :json do
include Rack::Test::Methods
let(:view_only_user) do
FactoryBot.create(:user,
member_in_project: project)
end
let(:edit_user) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: [:edit_project])
end
let(:non_member_user) do
FactoryBot.create(:user)
end
let(:project) { FactoryBot.create(:project, enabled_module_names: [:bcf]) }
subject(:response) { last_response }
describe 'GET /api/bcf/2.1/projects/:project_id' do
let(:path) { "/api/bcf/2.1/projects/#{project.id}" }
let(:current_user) { view_only_user }
before do
login_as(current_user)
get path
end
it_behaves_like 'bcf api successful response' do
let(:expected_body) do
{
project_id: project.id,
name: project.name
}
end
end
context 'lacking permissions' do
let(:current_user) { non_member_user }
it_behaves_like 'bcf api not found response'
end
end
describe 'PUT /api/bcf/2.1/projects/:project_id' do
let(:path) { "/api/bcf/2.1/projects/#{project.id}" }
let(:current_user) { edit_user }
let(:params) do
{
name: 'new project name'
}
end
before do
login_as(current_user)
put path, params.to_json
end
it_behaves_like 'bcf api successful response' do
let(:expected_body) do
{
project_id: project.id,
name: 'new project name'
}
end
end
context 'lacking view permissions' do
let(:current_user) { non_member_user }
it_behaves_like 'bcf api not found response'
end
context 'lacking edit permissions' do
let(:current_user) { view_only_user }
it_behaves_like 'bcf api not allowed response'
end
context 'attempting to alter the id' do
let(:params) do
{
project_id: 0
}
end
it_behaves_like 'bcf api unprocessable response' do
let(:message) { 'You must not write a read-only attribute.' }
end
end
end
describe 'GET /api/bcf/2.1/projects' do
let(:path) { "/api/bcf/2.1/projects" }
let(:current_user) { view_only_user }
let!(:invisible_project) { FactoryBot.create(:project, enabled_module_names: [:bcf]) }
let!(:non_bcf_project) do
FactoryBot.create(:project, enabled_module_names: [:work_packages]).tap do |p|
FactoryBot.create(:member,
project: p,
user: view_only_user,
roles: view_only_user.members.first.roles)
end
end
before do
login_as(current_user)
get path
end
it_behaves_like 'bcf api successful response' do
let(:expected_body) do
[
{
project_id: project.id,
name: project.name
}
]
end
end
end
end

@ -0,0 +1,107 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2019 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-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.
#++
shared_examples_for 'bcf api successful response' do
it 'responds 200 OK' do
expect(subject.status)
.to eql 200
end
it 'returns the project' do
expect(subject.body)
.to be_json_eql(expected_body.to_json)
end
it 'is has a json content type header' do
expect(subject.headers['Content-Type'])
.to eql 'application/json; charset=utf-8'
end
end
shared_examples_for 'bcf api not found response' do
it 'responds 404 NOT FOUND' do
expect(subject.status)
.to eql 404
end
it 'states a NOT FOUND message' do
expected = {
message: 'The requested resource could not be found.'
}
expect(subject.body)
.to be_json_eql(expected.to_json)
end
it 'is has a json content type header' do
expect(subject.headers['Content-Type'])
.to eql 'application/json; charset=utf-8'
end
end
shared_examples_for 'bcf api not allowed response' do
it 'responds 403 NOT ALLOWED' do
expect(subject.status)
.to eql 403
end
it 'states a NOT ALLOWED message' do
expected = {
message: 'You are not authorized to access this resource.'
}
expect(subject.body)
.to be_json_eql(expected.to_json)
end
it 'is has a json content type header' do
expect(subject.headers['Content-Type'])
.to eql 'application/json; charset=utf-8'
end
end
shared_examples_for 'bcf api unprocessable response' do
it 'responds 403 NOT ALLOWED' do
expect(subject.status)
.to eql 422
end
it 'states a reason message' do
expected = {
message: message
}
expect(subject.body)
.to be_json_eql(expected.to_json)
end
it 'is has a json content type header' do
expect(subject.headers['Content-Type'])
.to eql 'application/json; charset=utf-8'
end
end

@ -47,7 +47,6 @@ describe 'OAuth applications management', type: :feature, js: true do
fill_in 'application_redirect_uri', with: "not a url!"
click_on 'Create'
expect(page).to have_selector('.errorExplanation', text: 'Redirect URI must be an absolute URI.')
fill_in 'application_redirect_uri', with: "urn:ietf:wg:oauth:2.0:oob\nhttps://localhost/my/callback"
click_on 'Create'

Loading…
Cancel
Save