From 9af15711eff3de4fdf20141fb612eed3a2fa8382 Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 7 Nov 2019 15:51:43 +0100 Subject: [PATCH] introduce bcf project end point --- .../controllers/bcf/api/v2_1/projects_api.rb | 48 ++++ .../bcf/app/controllers/bcf/api/v2_1/root.rb | 253 ++++++++++++++++++ modules/bcf/config/routes.rb | 2 + modules/bcf/lib/open_project/bcf/engine.rb | 1 - .../bcf/v2_1/projects/projects_api_spec.rb | 60 +++++ 5 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb create mode 100644 modules/bcf/app/controllers/bcf/api/v2_1/root.rb create mode 100644 modules/bcf/spec/requests/api/bcf/v2_1/projects/projects_api_spec.rb diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb b/modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb new file mode 100644 index 0000000000..8fa93db0d7 --- /dev/null +++ b/modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb @@ -0,0 +1,48 @@ +#-- 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 + include OpenProject::StaticRouting::UrlHelpers + + resources :projects do + route_param :id, regexp: /\A(\d+)\z/ do + after_validation do + @project = Project.find(params[:id]) + end + + get do + { + project_id: @project.id, + name: @project.name + } + end + end + end + end +end \ No newline at end of file diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/root.rb b/modules/bcf/app/controllers/bcf/api/v2_1/root.rb new file mode 100644 index 0000000000..5de17f1910 --- /dev/null +++ b/modules/bcf/app/controllers/bcf/api/v2_1/root.rb @@ -0,0 +1,253 @@ +#-- 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. + +require 'open_project/authentication' + +module Bcf::API::V2_1 + class Root < Grape::API + extend ::API::Utilities::GrapeHelper + + class Formatter + def call(object, _env) + object.respond_to?(:to_json) ? object.to_json : MultiJson.dump(object) + 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 + + content_type :json, 'application/json; charset=utf-8' + format 'json' + formatter 'json', Formatter.new + + parser :json, Parser.new + + use ::OpenProject::Authentication::Manager + + helpers do + def current_user + User.current + end + + def declared_param(key) + declared(params)[key.to_s] + end + + def warden + env['warden'] + 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) + 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: 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') + api_error = ::API::Errors::Unauthenticated.new error_message + representer = ::API::V3::Errors::ErrorRepresenter.new api_error + + 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 + + # 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 + before do + # authenticate + User.current = User.find_by login: 'admin' + set_localization + enforce_content_type + end + + version '2.1', using: :path do + ## /auth + #mount ::OpenProject::Bcf::API::AuthEndpoint + ## /current-user + #mount ::OpenProject::Bcf::API::CurrentUserEndpoint + # /projects + mount ::Bcf::API::V2_1::ProjectsAPI + end + end +end diff --git a/modules/bcf/config/routes.rb b/modules/bcf/config/routes.rb index b0fa583345..e75aa62693 100644 --- a/modules/bcf/config/routes.rb +++ b/modules/bcf/config/routes.rb @@ -35,6 +35,8 @@ OpenProject::Application.routes.draw do scope '', as: 'bcf' do + mount Bcf::API::V2_1::Root => '/bcf' + scope 'projects/:project_id', as: 'project' do resources :issues, controller: 'bcf/issues' do get :upload, action: :upload, on: :collection diff --git a/modules/bcf/lib/open_project/bcf/engine.rb b/modules/bcf/lib/open_project/bcf/engine.rb index f6567344e7..6dd5fc6175 100644 --- a/modules/bcf/lib/open_project/bcf/engine.rb +++ b/modules/bcf/lib/open_project/bcf/engine.rb @@ -42,7 +42,6 @@ module OpenProject::Bcf default: { } } do - project_module :bcf do permission :view_linked_issues, { 'bcf/issues': %i[index] }, diff --git a/modules/bcf/spec/requests/api/bcf/v2_1/projects/projects_api_spec.rb b/modules/bcf/spec/requests/api/bcf/v2_1/projects/projects_api_spec.rb new file mode 100644 index 0000000000..1c1eceafc0 --- /dev/null +++ b/modules/bcf/spec/requests/api/bcf/v2_1/projects/projects_api_spec.rb @@ -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' + +describe 'BCF 2.1 projects resource', type: :request do + include Rack::Test::Methods + + let(:project) { FactoryBot.create(:project) } + subject(:response) { last_response } + + describe 'GET /bcf/2.1/projects/:project_id' do + let(:path) { "/bcf/2.1/projects/#{project.id}" } + + before do + get path + end + + it 'responds 200 OK' do + expect(subject.status) + .to eql 200 + end + + it 'returns the project' do + expected = { + project_id: project.id, + name: project.name + } + + expect(subject.body) + .to be_json_eql(expected.to_json) + end + end +end