Compare commits
20 Commits
dev
...
feature/fi
Author | SHA1 | Date |
---|---|---|
Wieland Lindenthal | 7c6b963100 | 3 years ago |
Wieland Lindenthal | 57aa2849f0 | 3 years ago |
Frank Bergmann | 14003960d6 | 3 years ago |
Frank Bergmann | a4542a7fb4 | 3 years ago |
Wieland Lindenthal | aa643a503f | 3 years ago |
Wieland Lindenthal | 183d7c3f0e | 3 years ago |
Frank Bergmann | 9126628949 | 3 years ago |
Wieland Lindenthal | 31ce3a0c1a | 3 years ago |
Frank Bergmann | c96f6d87a1 | 3 years ago |
Wieland Lindenthal | 41fb1c86d4 | 3 years ago |
Frank Bergmann | 5361109db8 | 3 years ago |
Wieland Lindenthal | 3bd5aec676 | 3 years ago |
Frank Bergmann | 120b3b6094 | 3 years ago |
Wieland Lindenthal | 0997f8840b | 3 years ago |
Wieland Lindenthal | 67d46eb730 | 3 years ago |
Wieland Lindenthal | 4f380a1441 | 3 years ago |
Wieland Lindenthal | 58e3744e6e | 3 years ago |
Frank Bergmann | 2733c0e064 | 3 years ago |
Frank Bergmann | cea5333a8f | 3 years ago |
Frank Bergmann | c6d3bdd336 | 3 years ago |
@ -0,0 +1,47 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module OAuthClients |
||||
module Concerns |
||||
module ManageStoragesGuarded |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
validate :validate_user_allowed_to_manage |
||||
|
||||
private |
||||
|
||||
def validate_user_allowed_to_manage |
||||
unless user.admin? && user.active? |
||||
errors.add :base, :error_unauthorized |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,49 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module OAuthClients |
||||
class CreateContract < ::ModelContract |
||||
include ActiveModel::Validations |
||||
include Concerns::ManageStoragesGuarded |
||||
|
||||
include ::OAuthClients::Concerns::ManageStoragesGuarded |
||||
include ActiveModel::Validations |
||||
|
||||
attribute :client_id, writable: true |
||||
validates :client_id, presence: true, length: { maximum: 255 } |
||||
|
||||
attribute :client_secret, writable: true |
||||
validates :client_secret, presence: true, length: { maximum: 255 } |
||||
|
||||
attribute :integration_type, writable: true |
||||
validates :integration_type, presence: true |
||||
|
||||
attribute :integration_id, writable: true |
||||
validates :integration_id, presence: true |
||||
end |
||||
end |
@ -0,0 +1,34 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
# See also: base_contract.rb for comments |
||||
module OAuthClients |
||||
class DeleteContract < ::DeleteContract |
||||
delete_permission :admin |
||||
end |
||||
end |
@ -0,0 +1,141 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
# This controller handles OAauth2 redirects from a provider to |
||||
# "callback" page. |
||||
class OAuthClientsController < ApplicationController |
||||
before_action :find_oauth_client |
||||
|
||||
# Provide the OAuth2 "callback" page: |
||||
# The OAuthClientsManager.get_token() method redirects |
||||
# here after successful authentication and authorization. |
||||
# This page gets a "code" parameter that cryptographically |
||||
# contains the approval. |
||||
# We get here by a URL like this: |
||||
# http://localhost:4200/oauth_client/1/oauth_callback? |
||||
# state=http%3A%2F%2Flocalhost%3A4200%2Fprojects%2Fdemo-project%2Foauth2_example& |
||||
# code=MQoOnUTJGFdAo5jBGD1SqnDH0PV6yioG7NoYM2zZZlK3g6LuKrGUmOxjIS1bIy7fHEfZy2WrgYcx |
||||
def callback |
||||
# oauth_client can be nil if OAuthClient was not found. |
||||
# This happens during admin setup if the user forgot to update the return_uri |
||||
# on Nextcloud after updating the OpenProject side with a new client_id and client_secret. |
||||
if !@oauth_client |
||||
flash[:error] = [I18n.t('oauth_client.errors.oauth_client_not_found'), |
||||
I18n.t('oauth_client.errors.oauth_client_not_found_explanation')] |
||||
redirect_to admin_settings_storages_path # Redirect to admin, because this only happens to sloppy admins |
||||
return |
||||
end |
||||
|
||||
# Extract the cryptographic "code" that indicates that the user |
||||
# has successfully authenticated agains the OAuth2 provider and has |
||||
# provided authorization to access his resources. |
||||
code = params[:code] |
||||
if code.blank? |
||||
# The OAuth2 provider should have sent a code when using response_type = "code" |
||||
# So this could either an error from the OAuth2 provider (Nextcloud) or |
||||
# ConnectionManager has used the wrong response_type. |
||||
flash[:error] = [I18n.t('oauth_client.errors.oauth_code_not_present'), |
||||
I18n.t('oauth_client.errors.oauth_code_not_present_explanation')] |
||||
redirect_to admin_settings_storages_path # Redirect to admin, because this only happens to sloppy admins |
||||
return |
||||
end |
||||
|
||||
# state is used by OpenProject to contain the redirection URL where to |
||||
# continue after receiving an OAuth2 token. So it should not be blank |
||||
state = params[:state] |
||||
if state.blank? |
||||
flash[:error] = [I18n.t('oauth_client.errors.oauth_state_not_present'), |
||||
I18n.t('oauth_client.errors.oauth_state_not_present_explanation')] |
||||
redirect_to admin_settings_storages_path # Redirect to admin, because this only happens to sloppy admins |
||||
return |
||||
end |
||||
|
||||
# Start the OAuth2 manager that will handle all the rest |
||||
connection_manager = OAuthClients::ConnectionManager.new(user: User.current, oauth_client: @oauth_client) |
||||
|
||||
# Exchange the code with a token using a HTTP call to the OAuth2 provider |
||||
service_result = connection_manager.code_to_token(code) |
||||
if service_result.success? |
||||
# Redirect the user to the page that initially wanted to access the OAuth2 resource. |
||||
# "state" is a variable that encapsulates the page's URL and status. |
||||
redirect_uri = connection_manager.callback_page_redirect_uri(params[:state]) |
||||
redirect_to redirect_uri |
||||
else |
||||
# We got a list of errors from ConnectionManger |
||||
flash[:error] = ["#{t(:'oauth_client.errors.oauth_was_a_mess')}:"] |
||||
service_result.errors.each do |error| |
||||
flash[:error] << "#{t(:'oauth_client.errors.oauth_reported')}: #{error.full_message}" |
||||
end |
||||
|
||||
redirect_user_or_admin(state) |
||||
end |
||||
end |
||||
|
||||
def refresh |
||||
# Test page for refreshing OAuth2 token |
||||
if !@oauth_client |
||||
flash[:error] = [I18n.t('oauth_client.errors.oauth_client_not_found'), |
||||
I18n.t('oauth_client.errors.oauth_client_not_found_explanation')] |
||||
redirect_to admin_settings_storages_path # Redirect to admin, because this only happens to sloppy admins |
||||
return |
||||
end |
||||
|
||||
# Start the OAuth2 manager that will handle all the rest |
||||
connection_manager = OAuthClients::ConnectionManager.new(user: User.current, oauth_client: @oauth_client) |
||||
connection_manager.refresh_token |
||||
|
||||
# ToDo: This redirect is only for admins |
||||
redirect_to admin_settings_storages_path(@oauth_client.integration) |
||||
end |
||||
|
||||
private |
||||
|
||||
def redirect_user_or_admin(state) |
||||
if User.current.admin |
||||
# ToDo: Check that integration a storage is |
||||
redirect_to admin_settings_storages_path(@oauth_client.integration) |
||||
else |
||||
flash[:error] = [t(:'oauth_client.errors.oauth_issue_contact_admin')] |
||||
redirect_to state |
||||
end |
||||
end |
||||
|
||||
# Storage ID coming from routes.rb preparsed |
||||
# Returns nil in case thae OAuthClient is not found. |
||||
# This can happen during setup if the user forgot to update |
||||
# the ID in the return_uri on the Nextcloud side. |
||||
def find_oauth_client |
||||
# During development we need to be able to identify the oauth_client by it's ID |
||||
@oauth_client = OAuthClient.find_by(id: params[:id]) |
||||
# Nextcloud "automaticial" configuration just adds the OAuth2 client_id as the Id |
||||
# so the Nextcloud side doesn't need to know the OpenProject object ID. |
||||
if @oauth_client.nil? |
||||
@oauth_client = OAuthClient.find_by(client_id: params[:id]) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,31 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
class OAuthClient < ApplicationRecord |
||||
belongs_to :integration, polymorphic: true |
||||
end |
@ -0,0 +1,48 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
# OAuthClientToken stores the OAuth2 Bearer+Refresh tokens that |
||||
# an OAuth2 server (Nextcloud or similar) provides after a user |
||||
# has granted access. |
||||
class OAuthClientToken < ApplicationRecord |
||||
# OAuthClientToken sits between User and OAuthClient |
||||
belongs_to :user |
||||
belongs_to :oauth_client |
||||
|
||||
# There should be only one token per project and oauth_client. |
||||
validates :user, presence: true |
||||
validates :user, uniqueness: { scope: :oauth_client } |
||||
|
||||
# There should be an oauth_client as parent |
||||
validates :oauth_client, presence: true |
||||
|
||||
# ToDo: Cover with model spec |
||||
validates :access_token, length: { minimum: 1, maximum: 255 } |
||||
# ToDo: Cover with model spec |
||||
validates :refresh_token, length: { minimum: 1, maximum: 255 } |
||||
end |
@ -0,0 +1,218 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require "rack/oauth2" |
||||
require "uri/http" |
||||
|
||||
module OAuthClients |
||||
class ConnectionManager |
||||
attr_reader :user, :rack_token, :oauth_client |
||||
|
||||
def initialize(user:, oauth_client:) |
||||
@user = user |
||||
@oauth_client = oauth_client |
||||
end |
||||
|
||||
# Main method to initiate the OAuth2 flow called by a "client" component |
||||
# that wants to access OAuth2 protected resources. |
||||
# Returns an OAuthClientToken object or a String in case a renew is required. |
||||
# @param state (OAuth2 RFC) encapsulates the state of the calling page (URL + params) to return |
||||
# @param scope (OAuth2 RFC) specifies the resources to access. Nextcloud only has one global scope. |
||||
def get_access_token(scope: [], state: nil) |
||||
# Check for an already existing token from last call |
||||
token = get_existing_token |
||||
return ServiceResult.new(success: true, result: token) if token.present? |
||||
|
||||
# Return a String with a redirect URL to Nextcloud instead of a token |
||||
@redirect_url = redirect_to_oauth_authorize(scope:, state:) |
||||
ServiceResult.new(success: false, result: @redirect_url) |
||||
end |
||||
|
||||
# The bearer/access token has expired or is due for renew for other reasons. |
||||
# Talk to OAuth2 provider to exchange the renew_token for a new bearer token. |
||||
def refresh_token |
||||
# There should already be an existing token, |
||||
# otherwise this method has been called too early (internal flow error). |
||||
oauth_client_token = get_existing_token |
||||
if oauth_client_token.nil? |
||||
return service_result_with_error(I18n.t('oauth_client.errors.refresh_token_called_without_existing_token')) |
||||
end |
||||
|
||||
# Get the Rack::OAuth2::Client and call access_token!, then return a ServiceResult. |
||||
service_result = request_new_token(refresh_token: oauth_client_token.refresh_token) |
||||
return service_result unless service_result.success? |
||||
|
||||
# Updated tokens, handle model checking errors and return a ServiceResult |
||||
update_oauth_client_token(oauth_client_token, service_result.result) |
||||
end |
||||
|
||||
# Redirect to the "authorize" endpoint of the OAuth2 provider. |
||||
# @param state (OAuth2 RFC) encapsulates the state of the calling page (URL + params) to return |
||||
# @param scope (OAuth2 RFC) specifies the resources to access. Nextcloud only has one global scope. |
||||
def redirect_to_oauth_authorize(scope: [], state: nil) |
||||
client = rack_oauth_client # Configure and start the rack-oauth2 client |
||||
client.authorization_uri(scope:, state:) |
||||
end |
||||
|
||||
# For the OAuth2 callback page: Calculate the redirection URL that will |
||||
# point the browser at the initial page that wanted to access the OAuth2 |
||||
# protected resource. |
||||
# @param state (OAuth2 RFC) encapsulates the state of the calling page (URL + params) to return |
||||
def callback_page_redirect_uri(state) |
||||
# In the current implementation "state" just consists of the URL of |
||||
# the initial page, possibily with "&var=value" added parameters. |
||||
# token is stored with the OAuth2::Manager, so we don't need it here. |
||||
|
||||
# So we can just return this page. |
||||
state |
||||
end |
||||
|
||||
# Called by callback_page with a cryptographic "code" that indicates |
||||
# that the user has successfully authorized the OAuth2 provider. |
||||
# We now are going to exchange this code to a token (bearer+refresh) |
||||
def code_to_token(code) |
||||
# Return a Rack::OAuth2::AccessToken::Bearer or an error string |
||||
service_result = request_new_token(authorization_code: code) |
||||
return service_result unless service_result.success? |
||||
|
||||
# Create a new OAuthClientToken from Rack::OAuth::AccessToken::Bearer and return |
||||
ServiceResult.new( |
||||
success: true, |
||||
result: create_new_oauth_client_token(service_result.result) |
||||
) |
||||
end |
||||
|
||||
private |
||||
|
||||
# Check if a OAuthClientToken already exists and return nil otherwise. |
||||
# Don't handle the case of an expired token. |
||||
def get_existing_token |
||||
# Check if we've got a token in the database and return nil otherwise. |
||||
OAuthClientToken.find_by(user_id: @user, oauth_client_id: @oauth_client.id) |
||||
end |
||||
|
||||
# Calls client.access_token! |
||||
# Convert the various exceptions into user-friendly error strings. |
||||
def request_new_token(options = {}) |
||||
rack_access_token = rack_oauth_client(options) |
||||
.access_token!(:body) # Rack::OAuth2::AccessToken |
||||
|
||||
ServiceResult.new(success: true, |
||||
result: rack_access_token) |
||||
rescue Rack::OAuth2::Client::Error => e # Handle Rack::OAuth2 specific errors |
||||
service_result_with_error(i18n_rack_oauth2_error_message(e)) |
||||
rescue Timeout::Error, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, |
||||
Errno::EINVAL, Errno::ENETUNREACH, Errno::ECONNRESET, Errno::ECONNREFUSED, JSON::ParserError => e |
||||
service_result_with_error( |
||||
"#{I18n.t('oauth_client.errors.oauth_returned_http_error')}: #{e.class}: #{e.message.to_html}" |
||||
) |
||||
rescue StandardError => e |
||||
service_result_with_error( |
||||
"#{I18n.t('oauth_client.errors.oauth_returned_standard_error')}: #{e.class}: #{e.message.to_html}" |
||||
) |
||||
end |
||||
|
||||
# Localize the error message |
||||
def i18n_rack_oauth2_error_message(rack_oauth2_client_exception) |
||||
l10n_key = "oauth_client.errors.rack_oauth2.#{rack_oauth2_client_exception.message}" |
||||
if I18n.exists? l10n_key |
||||
I18n.t(l10n_key) |
||||
else |
||||
"#{I18n.t('oauth_client.errors.oauth_returned_error')}: #{rack_oauth2_client_exception.message.to_html}" |
||||
end |
||||
end |
||||
|
||||
# Return a fully configured RackOAuth2Client. |
||||
# This client does all the heavy lifting with the OAuth2 protocol. |
||||
def rack_oauth_client(options = {}) |
||||
oauth_client_uri = URI.parse(@oauth_client.integration.host) |
||||
oauth_client_scheme = oauth_client_uri.scheme |
||||
oauth_client_host = oauth_client_uri.host |
||||
oauth_client_port = oauth_client_uri.port |
||||
|
||||
client = Rack::OAuth2::Client.new( |
||||
identifier: @oauth_client.client_id, |
||||
secret: @oauth_client.client_secret, |
||||
scheme: oauth_client_scheme, |
||||
host: oauth_client_host, |
||||
port: oauth_client_port, |
||||
authorization_endpoint: "/apps/oauth2/authorize", |
||||
token_endpoint: "/apps/oauth2/api/v1/token" |
||||
) |
||||
|
||||
# Write options, for example authorization_code and refresh_token |
||||
client.refresh_token = options[:refresh_token] if options[:refresh_token] |
||||
client.authorization_code = options[:authorization_code] if options[:authorization_code] |
||||
client |
||||
end |
||||
|
||||
# Create a new OpenProject token object based on the return values |
||||
# from a Rack::OAuth2::AccessToken::Bearer token |
||||
def create_new_oauth_client_token(rack_access_token) |
||||
OAuthClientToken.create( |
||||
user: @user, |
||||
oauth_client: @oauth_client, |
||||
origin_user_id: rack_access_token.raw_attributes[:user_id], # ID of user at OAuth2 provider |
||||
access_token: rack_access_token.access_token, |
||||
token_type: rack_access_token.token_type, # :bearer |
||||
refresh_token: rack_access_token.refresh_token, |
||||
expires_in: rack_access_token.raw_attributes[:expires_in], |
||||
scope: rack_access_token.scope, # nil |
||||
state: "undefined" |
||||
) |
||||
end |
||||
|
||||
# Update an OpenProject token based on updated values from a |
||||
# Rack::OAuth2::AccessToken::Bearer after a OAuth2 refresh operation |
||||
# ToDo: copy into service |
||||
def update_oauth_client_token(oauth_client_token, rack_oauth2_access_token) |
||||
success = oauth_client_token.update( |
||||
access_token: rack_oauth2_access_token.access_token, |
||||
refresh_token: rack_oauth2_access_token.refresh_token, |
||||
expires_in: rack_oauth2_access_token.expires_in |
||||
) |
||||
|
||||
if success |
||||
ServiceResult.new(success: true, result: oauth_client_token) |
||||
else |
||||
result = ServiceResult.new(success: false) |
||||
result.errors.add(:base, I18n.t('oauth_client.errors.refresh_token_updated_failed')) |
||||
result.add_dependent!(ServiceResult.new(success: false, errors: oauth_client_token.errors)) |
||||
result |
||||
end |
||||
end |
||||
|
||||
# Shortcut method to convert an error message into an unsuccessful |
||||
# ServiceResult with that error message |
||||
def service_result_with_error(message) |
||||
ServiceResult.new(success: false).tap do |result| |
||||
result.errors.add(:base, message) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,39 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
# The logic for creating storage was extracted from the controller and put into |
||||
# a service: https://dev.to/joker666/ruby-on-rails-pattern-service-objects-b19 |
||||
# Purpose: create and persist a Storages::Storage record |
||||
# Used by: Storages::Admin::StoragesController#create, could also be used by the |
||||
# API in the future. |
||||
# Reference: https://www.openproject.org/docs/development/concepts/contracted-services/ |
||||
# The comments here are also valid for the other *_service.rb files |
||||
module OAuthClients |
||||
class CreateService < ::BaseServices::Create |
||||
end |
||||
end |
@ -0,0 +1,33 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
# See also: create_service.rb for comments |
||||
module OAuthClients |
||||
class DeleteService < ::BaseServices::Delete |
||||
end |
||||
end |
@ -0,0 +1,31 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
# See also: create_service.rb for comments |
||||
class OAuthClients::SetAttributesService < ::BaseServices::SetAttributes |
||||
end |
@ -0,0 +1,40 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
class CreateOAuthClient < ActiveRecord::Migration[6.1] |
||||
def change |
||||
create_table :oauth_clients do |t| |
||||
t.string :client_id, null: false |
||||
t.string :client_secret, null: false |
||||
t.references :integration, |
||||
polymorphic: true, index: { unique: true } |
||||
|
||||
t.timestamps |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,19 @@ |
||||
class CreateOAuthClientTokens < ActiveRecord::Migration[6.1] |
||||
def change |
||||
create_table :oauth_client_tokens do |t| |
||||
t.references :oauth_client, null: false, foreign_key: { to_table: :oauth_clients, on_delete: :cascade } |
||||
t.references :user, null: false, index: true, foreign_key: { to_table: :users, on_delete: :cascade } |
||||
|
||||
t.string :access_token |
||||
t.string :refresh_token |
||||
t.string :token_type |
||||
t.integer :expires_in |
||||
t.string :scope |
||||
t.string :state |
||||
t.string :origin_user_id # ID of the current user on the _OAuth2_provider_side_ |
||||
|
||||
t.timestamps |
||||
t.index %i[user_id oauth_client_id], unique: true |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,109 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
class Storages::Admin::OAuthClientsController < ApplicationController |
||||
# See https://guides.rubyonrails.org/layouts_and_rendering.html for reference on layout |
||||
layout 'admin' |
||||
|
||||
# Before executing any action below: Make sure the current user is an admin |
||||
# and set the @<controller_name> variable to the object referenced in the URL. |
||||
before_action :require_admin |
||||
|
||||
before_action :find_storage |
||||
before_action :delete_current_oauth_client, only: %i[create] |
||||
|
||||
# menu_item is defined in the Redmine::MenuManager::MenuController |
||||
# module, included from ApplicationController. |
||||
# The menu item is defined in the engine.rb |
||||
menu_item :storages_admin_settings |
||||
|
||||
# Show the admin page to create a new OAuthClient object. |
||||
def new |
||||
@oauth_client = ::OAuthClients::SetAttributesService.new(user: User.current, |
||||
model: OAuthClient.new, |
||||
contract_class: EmptyContract) |
||||
.call |
||||
.result |
||||
render '/storages/admin/storages/new_oauth_client' |
||||
end |
||||
|
||||
# Actually create a OAuthClient object. |
||||
# Use service pattern to create a new OAuthClient |
||||
# See also: https://www.openproject.org/docs/development/concepts/contracted-services/ |
||||
# Called by: Global app/config/routes.rb to serve Web page |
||||
def create |
||||
service_result = ::OAuthClients::CreateService.new(user: User.current) |
||||
.call(permitted_oauth_client_params.merge(integration: @storage)) |
||||
@oauth_client = service_result.result |
||||
if service_result.success? |
||||
flash[:notice] = I18n.t(:notice_successful_create) |
||||
# admin_settings_storage_path is automagically created by Ruby routes. |
||||
redirect_to admin_settings_storage_path(@storage) |
||||
else |
||||
@errors = service_result.errors |
||||
render '/storages/admin/storages/new_oauth_client' |
||||
end |
||||
end |
||||
|
||||
# Used by: admin layout |
||||
# Breadcrumbs is something like OpenProject > Admin > Storages. |
||||
# This returns the name of the last part (Storages admin page) |
||||
def default_breadcrumb |
||||
ActionController::Base.helpers.link_to(t('storages.label_oauth_client_details'), admin_settings_storage_oauth_client_path) |
||||
end |
||||
|
||||
# See: default_breadcrumb above |
||||
# Defines whether to show breadcrumbs on the page or not. |
||||
def show_local_breadcrumb |
||||
true |
||||
end |
||||
|
||||
private |
||||
|
||||
def ensure_storages_module_active |
||||
return if OpenProject::FeatureDecisions.storages_module_active? |
||||
|
||||
raise ActionController::RoutingError, 'Not Found' |
||||
end |
||||
|
||||
# Called by create and update above in order to check if the |
||||
# update parameters are correctly set. |
||||
def permitted_oauth_client_params |
||||
params |
||||
.require(:oauth_client) |
||||
.permit('client_id', 'client_secret') |
||||
end |
||||
|
||||
def find_storage |
||||
@storage = ::Storages::Storage.find(params[:storage_id]) |
||||
end |
||||
|
||||
def delete_current_oauth_client |
||||
::OAuthClients::DeleteService.new(user: User.current, model: @storage.oauth_client).call if @storage.oauth_client |
||||
end |
||||
end |
@ -0,0 +1,38 @@ |
||||
<!-- Standard Ruby view, please see the controller for comments --> |
||||
<% html_title t(:label_administration), t("project_module_storages"), @storage.name, "#{t("storages.provider_types.#{@storage.provider_type}")} #{t("storages.label_oauth_client_details")}" %> |
||||
<% local_assigns[:additional_breadcrumb] = "#{t("storages.provider_types.#{@storage.provider_type}")} #{t("storages.label_oauth_client_details")}" %> |
||||
<%= toolbar title: "#{t("storages.provider_types.#{@storage.provider_type}")} #{t("storages.label_oauth_client_details")}" %> |
||||
|
||||
<%= labelled_tabular_form_for @oauth_client, url: admin_settings_storage_oauth_client_path do |f| -%> |
||||
<div class="form--field -required"> |
||||
<%= f.text_field :client_id, |
||||
label: t('storages.label_oauth_client_id'), |
||||
required: true, |
||||
size: 40, |
||||
container_class: '-wide' %> |
||||
<span class="form--field-instructions"> |
||||
<%= t("storages.instructions.#{@storage.provider_type}.oauth_client_id") %> |
||||
<%= link_to "#{t("storages.provider_types.#{@storage.provider_type}")} / #{t("storages.instructions.#{@storage.provider_type}.administration")} / #{t("storages.instructions.#{@storage.provider_type}.oauth2_clients")}", |
||||
URI::join(@storage.host, "settings/admin/security#oauth2").to_s, |
||||
target: "blank" %> |
||||
</span> |
||||
</div> |
||||
<div class="form--field -required"> |
||||
<%= f.text_field :client_secret, |
||||
label: t('storages.label_oauth_client_secret'), |
||||
required: true, |
||||
size: 40, |
||||
container_class: '-wide' %> |
||||
<span class="form--field-instructions"> |
||||
<%= t("storages.instructions.#{@storage.provider_type}.oauth_client_secret") %> |
||||
<%= link_to "#{t("storages.provider_types.#{@storage.provider_type}")} / #{t("storages.instructions.#{@storage.provider_type}.administration")} / #{t("storages.instructions.#{@storage.provider_type}.oauth2_clients")}", |
||||
URI::join(@storage.host, "settings/admin/security#oauth2").to_s, |
||||
target: "blank" %> |
||||
</span> |
||||
</div> |
||||
<% if @storage.oauth_client %> |
||||
<%= styled_button_tag t(:button_replace), class: "-highlight -with-icon icon-checkmark" %> |
||||
<% else %> |
||||
<%= styled_button_tag t(:button_save), class: "-highlight -with-icon icon-checkmark" %> |
||||
<% end %> |
||||
<% end %> |
@ -0,0 +1,90 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require_module_spec_helper |
||||
require 'contracts/shared/model_contract_shared_context' |
||||
|
||||
describe ::OAuthClients::CreateContract do |
||||
include_context 'ModelContract shared context' |
||||
|
||||
let(:current_user) { create(:admin) } |
||||
let(:client_id) { "1234567889" } |
||||
let(:client_secret) { "asdfasdfasdf" } |
||||
let(:integration) { build_stubbed :storage } |
||||
let(:oauth_client) do |
||||
build :oauth_client, |
||||
client_id: client_id, |
||||
client_secret: client_secret, |
||||
integration: integration |
||||
end |
||||
|
||||
let(:contract) { described_class.new(oauth_client, current_user) } |
||||
|
||||
it_behaves_like 'contract is valid for active admins and invalid for regular users' |
||||
|
||||
describe 'validations' do |
||||
context 'when all attributes are valid' do |
||||
include_examples 'contract is valid' |
||||
end |
||||
|
||||
%i[client_id client_secret].each do |attribute_name| |
||||
context 'when client_id is invalid' do |
||||
context 'as it is too long' do |
||||
let(attribute_name) { 'X' * 257 } |
||||
|
||||
include_examples 'contract is invalid', attribute_name => :too_long |
||||
end |
||||
|
||||
context 'as it is empty' do |
||||
let(attribute_name) { '' } |
||||
|
||||
include_examples 'contract is invalid', attribute_name => :blank |
||||
end |
||||
|
||||
context 'as it is nil' do |
||||
let(attribute_name) { nil } |
||||
|
||||
include_examples 'contract is invalid', attribute_name => :blank |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'with integration (polymorphic attribute) linked' do |
||||
let(:integration) { create :storage } |
||||
|
||||
include_examples 'contract is valid' |
||||
end |
||||
|
||||
context 'without integration (polymorphic attribute)' do |
||||
let(:integration) { nil } |
||||
|
||||
include_examples 'contract is invalid', { integration_id: :blank, integration_type: :blank } |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,43 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require_module_spec_helper |
||||
require 'contracts/shared/model_contract_shared_context' |
||||
|
||||
# This DeleteContract spec just tests if the user is _allowed_ |
||||
# to execute the operation. |
||||
describe ::OAuthClients::DeleteContract do |
||||
include_context 'ModelContract shared context' |
||||
|
||||
let(:oauth_client) { create :oauth_client } |
||||
let(:contract) { described_class.new(oauth_client, current_user) } |
||||
|
||||
# Generic checks that the contract is valid for valid admin, but invalid otherwise |
||||
it_behaves_like 'contract is valid for active admins and invalid for regular users' |
||||
end |
@ -0,0 +1,35 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
FactoryBot.define do |
||||
factory :oauth_client, class: '::OAuthClient' do |
||||
sequence(:client_id) { |n| "1234567890-#{n}" } |
||||
sequence(:client_secret) { |n| "2345678901-#{n}" } |
||||
integration factory: :storage |
||||
end |
||||
end |
@ -0,0 +1,36 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
FactoryBot.define do |
||||
factory :oauth_client_token, class: '::OAuthClientToken' do |
||||
sequence(:access_token) { |n| "1234567890-#{n}" } |
||||
sequence(:refresh_token) { |n| "2345678901-#{n}" } |
||||
oauth_client factory: :oauth_client |
||||
user factory: :user |
||||
end |
||||
end |
@ -0,0 +1,111 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe OAuthClientToken, type: :model do |
||||
let(:access_token) { "x" } |
||||
let(:refresh_token) { "x" } |
||||
let(:user) { create :user } |
||||
let(:oauth_client) { create :oauth_client } |
||||
let(:instance) { described_class.new(access_token:, refresh_token:, user:, oauth_client:) } |
||||
|
||||
describe '#valid?' do |
||||
subject { instance.valid? } |
||||
|
||||
context 'with default arguments' do |
||||
it 'succeeds' do |
||||
expect(subject).to be_truthy |
||||
end |
||||
end |
||||
|
||||
context 'with access_token too long' do |
||||
let(:access_token) { "x" * 257 } |
||||
|
||||
it 'fails with access_token too long' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
|
||||
context 'with refresh_token too long' do |
||||
let(:refresh_token) { "x" * 257 } |
||||
|
||||
it 'fails with refresh_token too long' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
|
||||
context 'with access_token too short' do |
||||
let(:access_token) { "" } |
||||
|
||||
it 'fails with access_token too short' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
|
||||
context 'with refresh_token too short' do |
||||
let(:refresh_token) { "" } |
||||
|
||||
it 'fails with refresh_token too short' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
|
||||
context 'without access_token' do |
||||
let(:access_token) { nil } |
||||
|
||||
it 'fails with access_token is nil' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
|
||||
context 'without refresh_token' do |
||||
let(:refresh_token) { nil } |
||||
|
||||
it 'fails with refresh_token is nil' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
|
||||
context 'with invalid user' do |
||||
let(:user) { nil } |
||||
|
||||
it 'fails with inavlid user' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
|
||||
context 'with invalid oauth_client' do |
||||
let(:oauth_client) { nil } |
||||
|
||||
it 'fails with invalid oauth_client' do |
||||
expect(subject).to be_falsey |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,337 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'webmock/rspec' |
||||
|
||||
describe ::OAuthClients::ConnectionManager, type: :model do |
||||
let(:user) { create :user } |
||||
let(:host_ip) { "172.16.193.144" } |
||||
let(:host_proto) { "http://" } |
||||
let(:host_port) { "" } |
||||
let(:host) { "#{host_proto}#{host_ip}#{host_port}" } |
||||
let(:provider_type) { ::Storages::Storage::PROVIDER_TYPE_NEXTCLOUD } |
||||
let(:storage) { create(:storage, provider_type:, host: "#{host}/") } |
||||
let(:scope) { [:all] } # OAuth2 resources to access, specific to provider |
||||
let(:oauth_client) do |
||||
create(:oauth_client, |
||||
client_id: "nwz34rWsolvJvchfQ1bVHXfMb1ETK89lCBgzrLhWx3ACW5nKfmdcyf5ftlCyKGbk", |
||||
client_secret: "A08n6CRBOOr41iqkWRynnP6BbmEnau7LeP9t9xrIbiYX46iXgmIZgqhJoDFjUMEq", |
||||
integration: storage) |
||||
end |
||||
let(:oauth_client_token) { create(:oauth_client_token, oauth_client:, user:) } |
||||
let(:instance) { described_class.new(user:, oauth_client:) } |
||||
|
||||
# Test the redirect_to_oauth_authorize function that puts together |
||||
# the OAuth2 provider URL (Nextcloud) according to RFC specs. |
||||
describe '#redirect_to_oauth_authorize' do |
||||
let(:scope) { nil } |
||||
let(:state) { nil } |
||||
|
||||
subject { instance.redirect_to_oauth_authorize(scope:, state:) } |
||||
|
||||
context 'with empty state and scope' do |
||||
it 'returns the redirect URL' do |
||||
expect(subject).to be_a String |
||||
expect(subject).to include oauth_client.integration.host |
||||
expect(subject).not_to include "scope" |
||||
expect(subject).not_to include "state" |
||||
end |
||||
end |
||||
|
||||
context 'with state but empty scope' do |
||||
let(:state) { "https://example.com/page" } |
||||
|
||||
it 'returns the redirect URL' do |
||||
expect(subject).to be_a String |
||||
expect(subject).to include oauth_client.integration.host |
||||
expect(subject).not_to include "scope" |
||||
expect(subject).to include "&state=https" |
||||
end |
||||
end |
||||
|
||||
context 'with multiple scopes but empty state' do |
||||
let(:scope) { %i(email profile) } |
||||
|
||||
it 'returns the redirect URL' do |
||||
expect(subject).to be_a String |
||||
expect(subject).to include oauth_client.integration.host |
||||
expect(subject).not_to include "state" |
||||
expect(subject).to include "&scope=email%20profile" |
||||
end |
||||
end |
||||
end |
||||
|
||||
# The first step in the OAuth2 flow is to produce a URL for the |
||||
# user to authenticate and authorize access at the OAuth2 provider |
||||
# (Nextcloud). |
||||
describe '#get_access_token' do |
||||
subject { instance.get_access_token } |
||||
|
||||
context 'with no OAuthClientToken present' do |
||||
it 'returns a redirection URL' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_a String |
||||
# Details of string are tested above in section #redirect_to_oauth_authorize |
||||
end |
||||
end |
||||
|
||||
context 'with no OAuthClientToken present and state parameters' do |
||||
subject { instance.get_access_token(state: "some_state", scope: [:email]) } |
||||
|
||||
it 'returns the redirect URL' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_a String |
||||
expect(subject.result).to include oauth_client.integration.host |
||||
expect(subject.result).to include "&state=some_state" |
||||
expect(subject.result).to include "&scope=email" |
||||
end |
||||
end |
||||
|
||||
context 'with an OAuthClientToken present' do |
||||
before do |
||||
oauth_client_token |
||||
end |
||||
|
||||
it 'returns the OAuthClientToken' do |
||||
expect(subject).to be_truthy |
||||
expect(subject.result).to be_a OAuthClientToken # The one and only... |
||||
expect(subject.result).to eql oauth_client_token |
||||
end |
||||
end |
||||
end |
||||
|
||||
# In the second step the OAuth2 provider (Nextcloud) redirects |
||||
# to a "callback" endpoint on the OAuth2 client (OpenpPoject): |
||||
# http://<openproject>:4200/oauth_clients/8/callback?state=&code=7kRGJ...jG3KZ |
||||
# This callback code basically just calls code_to_token(code). |
||||
# The callback endpoint calls code_to_token(code) with the code |
||||
# received and exchanges the code for a bearer+refresh token |
||||
# using a HTTP request. |
||||
describe '#code_to_token' do |
||||
let(:code) { "7kRGJ...jG3KZ" } |
||||
|
||||
subject { instance.code_to_token(code) } |
||||
|
||||
context 'with happy path' do |
||||
before do |
||||
# Simulate a successful authorization returning the tokens |
||||
response_body = { |
||||
access_token: "yjTDZ...RYvRH", |
||||
token_type: "Bearer", |
||||
expires_in: 3600, |
||||
refresh_token: "UwFp...1FROJ", |
||||
user_id: "admin" |
||||
}.to_json |
||||
stub_request(:any, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return(status: 200, body: response_body) |
||||
end |
||||
|
||||
it 'returns a valid ClientToken object', webmock: true do |
||||
expect(subject.success).to be_truthy |
||||
expect(subject.result).to be_a OAuthClientToken |
||||
end |
||||
end |
||||
|
||||
context 'with known reply invalid_request', webmock: true do |
||||
before do |
||||
stub_request(:post, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return(status: 400, body: { error: "invalid_request" }.to_json) |
||||
end |
||||
|
||||
it 'returns a specific error message' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors[:base].count).to be(1) |
||||
expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.rack_oauth2.invalid_request') |
||||
expect(subject.errors[:base].first).not_to include I18n.t('oauth_client.errors.oauth_returned_error') |
||||
end |
||||
end |
||||
|
||||
context 'with unknown reply', webmock: true do |
||||
before do |
||||
stub_request(:post, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return(status: 400, body: { error: "invalid_requesttt" }.to_json) |
||||
end |
||||
|
||||
it 'returns an unspecific error message' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors[:base].count).to be(1) |
||||
expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.oauth_returned_error') |
||||
end |
||||
end |
||||
|
||||
context 'with reply including JSON syntax error', webmock: true do |
||||
before do |
||||
stub_request(:post, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return( |
||||
status: 400, |
||||
headers: { 'Content-Type' => 'application/json; charset=utf-8' }, |
||||
body: "some: very, invalid> <json}" |
||||
) |
||||
end |
||||
|
||||
it 'returns an unspecific error message' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors[:base].count).to be(1) |
||||
expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.oauth_returned_error') |
||||
end |
||||
end |
||||
|
||||
context 'with 500 reply without body', webmock: true do |
||||
before do |
||||
stub_request(:post, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return(status: 500) |
||||
end |
||||
|
||||
it 'returns an unspecific error message' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors[:base].count).to be(1) |
||||
expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.oauth_returned_error') |
||||
end |
||||
end |
||||
|
||||
context 'with bad HTTP response', webmock: true do |
||||
before do |
||||
stub_request(:post, File.join(host, '/apps/oauth2/api/v1/token')).to_raise(Net::HTTPBadResponse) |
||||
end |
||||
|
||||
it 'returns an unspecific error message' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors[:base].count).to be(1) |
||||
expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.oauth_returned_http_error') |
||||
end |
||||
end |
||||
|
||||
context 'with timeout returns internal error', webmock: true do |
||||
before do |
||||
stub_request(:post, File.join(host, '/apps/oauth2/api/v1/token')).to_timeout |
||||
end |
||||
|
||||
it 'returns an unspecific error message' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors[:base].count).to be(1) |
||||
expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.oauth_returned_standard_error') |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#refresh_token' do |
||||
subject { instance.refresh_token } |
||||
|
||||
context 'without preexisting OAuthClientToken' do |
||||
it 'returns an error message' do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.errors[:base].first) |
||||
.to include I18n.t('oauth_client.errors.refresh_token_called_without_existing_token') |
||||
end |
||||
end |
||||
|
||||
context 'with successful response from OAuth2 provider (happy path)' do |
||||
before do |
||||
# Simulate a successful authorization returning the tokens |
||||
response_body = { |
||||
access_token: "xyjTDZ...RYvRH", |
||||
token_type: "Bearer", |
||||
expires_in: 3601, |
||||
refresh_token: "xUwFp...1FROJ", |
||||
user_id: "admin" |
||||
}.to_json |
||||
stub_request(:any, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return(status: 200, body: response_body) |
||||
oauth_client_token |
||||
end |
||||
|
||||
it 'returns a valid ClientToken object', webmock: true do |
||||
expect(subject.success).to be_truthy |
||||
expect(subject.result).to be_a OAuthClientToken |
||||
expect(subject.result.access_token).to eq("xyjTDZ...RYvRH") |
||||
expect(subject.result.refresh_token).to eq("xUwFp...1FROJ") |
||||
expect(subject.result.expires_in).to be(3601) |
||||
end |
||||
end |
||||
|
||||
context 'with invalid access_token data' do |
||||
before do |
||||
# Simulate a token too long |
||||
response_body = { |
||||
access_token: "x" * 257, # will fail model validation |
||||
token_type: "Bearer", |
||||
expires_in: 3601, |
||||
refresh_token: "xUwFp...1FROJ", |
||||
user_id: "admin" |
||||
}.to_json |
||||
stub_request(:any, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return(status: 200, body: response_body) |
||||
|
||||
oauth_client_token |
||||
end |
||||
|
||||
it 'returns dependent error from model validation', webmock: true do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors.size).to be(1) |
||||
puts subject.errors |
||||
end |
||||
end |
||||
|
||||
context 'with server error from OAuth2 provider' do |
||||
before do |
||||
stub_request(:any, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_return(status: 400, body: { error: "invalid_request" }.to_json) |
||||
oauth_client_token |
||||
end |
||||
|
||||
it 'returns a server error', webmock: true do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.errors.size).to be(1) |
||||
puts subject.errors |
||||
end |
||||
end |
||||
|
||||
context 'with successful response but invalid data' do |
||||
before do |
||||
# Simulate timeout |
||||
stub_request(:any, File.join(host, '/apps/oauth2/api/v1/token')) |
||||
.to_timeout |
||||
oauth_client_token |
||||
end |
||||
|
||||
it 'returns a valid ClientToken object', webmock: true do |
||||
expect(subject.success).to be_falsey |
||||
expect(subject.result).to be_nil |
||||
expect(subject.errors.size).to be(1) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,36 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'services/base_services/behaves_like_create_service' |
||||
|
||||
describe ::OAuthClients::CreateService, type: :model do |
||||
it_behaves_like 'BaseServices create service' do |
||||
let(:factory) { :oauth_client } |
||||
end |
||||
end |
@ -0,0 +1,36 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'services/base_services/behaves_like_delete_service' |
||||
|
||||
describe ::OAuthClients::DeleteService, type: :model do |
||||
it_behaves_like 'BaseServices delete service' do |
||||
let(:factory) { :oauth_client } |
||||
end |
||||
end |
@ -0,0 +1,114 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::OAuthClients::SetAttributesService, type: :model do |
||||
let(:current_user) { build_stubbed(:admin) } |
||||
|
||||
let(:contract_instance) do |
||||
contract = instance_double(::OAuthClients::CreateContract, 'contract_instance') |
||||
allow(contract) |
||||
.to receive(:validate) |
||||
.and_return(contract_valid) |
||||
allow(contract) |
||||
.to receive(:errors) |
||||
.and_return(contract_errors) |
||||
contract |
||||
end |
||||
|
||||
let(:contract_errors) { instance_double(ActiveModel::Errors, 'contract_errors') } |
||||
let(:contract_valid) { true } |
||||
let(:model_valid) { true } |
||||
|
||||
let(:instance) do |
||||
described_class.new(user: current_user, |
||||
model: model_instance, |
||||
contract_class: contract_class, |
||||
contract_options: {}) |
||||
end |
||||
let(:model_instance) { ::OAuthClient.new } |
||||
let(:contract_class) do |
||||
allow(::OAuthClients::CreateContract) |
||||
.to receive(:new) |
||||
.and_return(contract_instance) |
||||
|
||||
::OAuthClients::CreateContract |
||||
end |
||||
|
||||
let(:params) { {} } |
||||
|
||||
before do |
||||
allow(model_instance) |
||||
.to receive(:valid?) |
||||
.and_return(model_valid) |
||||
end |
||||
|
||||
subject { instance.call(params) } |
||||
|
||||
it 'returns the instance as the result' do |
||||
expect(subject.result) |
||||
.to eql model_instance |
||||
end |
||||
|
||||
it 'is a success' do |
||||
expect(subject) |
||||
.to be_success |
||||
end |
||||
|
||||
context 'with params' do |
||||
let(:params) do |
||||
{ |
||||
client_id: '0123456789-client_id', |
||||
client_secret: '1234567890-client_secret' |
||||
} |
||||
end |
||||
|
||||
before do |
||||
subject |
||||
end |
||||
|
||||
it 'assigns the params' do |
||||
expect(model_instance.client_id).to eq '0123456789-client_id' |
||||
expect(model_instance.client_secret).to eq '1234567890-client_secret' |
||||
end |
||||
end |
||||
|
||||
context 'with an invalid contract' do |
||||
let(:contract_valid) { false } |
||||
|
||||
it 'returns failure' do |
||||
expect(subject).not_to be_success |
||||
end |
||||
|
||||
it "returns the contract's errors" do |
||||
expect(subject.errors) |
||||
.to eql(contract_errors) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue