Added authentication method selection in integration setup (#737)

* added oidc based authorization in integration

Signed-off-by: Sagar <sagargurung1001@gmail.com>

* Added change log

Signed-off-by: Sagar <sagargurung1001@gmail.com>

* review address PR

Signed-off-by: Sagar <sagargurung1001@gmail.com>

* Added link to user oidc app

Signed-off-by: Sagar <sagargurung1001@gmail.com>

---------

Signed-off-by: Sagar <sagargurung1001@gmail.com>
This commit is contained in:
Sagar Gurung 2025-01-10 12:10:11 +05:45 коммит произвёл GitHub
Родитель fb3c4231fd
Коммит c1384c2f6c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
30 изменённых файлов: 3586 добавлений и 428 удалений

2
.github/workflows/shared_workflow.yml поставляемый
Просмотреть файл

@ -116,10 +116,12 @@ jobs:
- name: PHP & Vue Unit Tests
run: |
git clone --depth 1 https://github.com/nextcloud/groupfolders.git -b ${{ matrix.nextcloudVersion }} server/apps/groupfolders
git clone --depth 1 https://github.com/nextcloud/user_oidc.git server/apps/user_oidc
mkdir -p server/apps/integration_openproject
cp -r `ls -A | grep -v 'server'` server/apps/integration_openproject/
cd server
./occ a:e groupfolders
./occ a:e user_oidc
./occ a:e integration_openproject
cd apps/integration_openproject
# The following if block can be removed once Nextcloud no longer supports PHP 8.0

Просмотреть файл

@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Changed
- Add application's support for Nextcloud 31
- Add support for OIDC-based connection between Nextcloud and OpenProject
## 2.7.2 - 2024-12-16
### Fixed

Просмотреть файл

@ -142,6 +142,30 @@ class ConfigController extends Controller {
$this->config->deleteUserValue($userId, Application::APP_ID, 'refresh_token');
}
/**
* @return void
*/
public function resetOauth2Configs(): void {
// for oauth2 reset we reset openproject client credential as well as nextcloud client credential
$this->config->setAppValue(Application::APP_ID, 'openproject_client_id', "");
$this->config->setAppValue(Application::APP_ID, 'openproject_client_secret', "");
$this->deleteOauthClient();
}
/**
* @return void
*/
public function resetOIDCConfigs(string $userId = null): void {
if ($userId === null) {
$userId = $this->userId;
}
$this->config->setAppValue(Application::APP_ID, 'oidc_provider', "");
$this->config->setAppValue(Application::APP_ID, 'targeted_audience_client_id', "");
$this->config->deleteUserValue($userId, Application::APP_ID, 'user_id');
$this->config->deleteUserValue($userId, Application::APP_ID, 'user_name');
}
/**
* set config values
* @NoAdminRequired
@ -184,6 +208,9 @@ class ConfigController extends Controller {
private function setIntegrationConfig(array $values): array {
$allowedKeys = [
'openproject_instance_url',
'authorization_method',
'oidc_provider',
'targeted_audience_client_id',
'openproject_client_id',
'openproject_client_secret',
'default_enable_navigation',
@ -233,17 +260,72 @@ class ConfigController extends Controller {
);
}
}
$oldClientId = $oldClientSecret = '';
$oldOpenProjectOauthUrl = $this->config->getAppValue(
Application::APP_ID, 'openproject_instance_url', ''
);
$oldClientId = $this->config->getAppValue(
Application::APP_ID, 'openproject_client_id', ''
$oldAuthMethod = $this->config->getAppValue(
Application::APP_ID, 'authorization_method', ''
);
$oldClientSecret = $this->config->getAppValue(
Application::APP_ID, 'openproject_client_secret', ''
// when 'openproject_instance_url' && 'authorization_method' does not have a value at the same time,
// it means a full reset is done
$runningFullReset = key_exists('openproject_instance_url', $values) &&
key_exists('authorization_method', $values) &&
!$values['openproject_instance_url'] &&
!$values['authorization_method'];
// determine if the full reset is done when configuration is already with "oauth2"
$runningFullResetWithOAuth2Auth = (
$runningFullReset &&
$oldAuthMethod === OpenProjectAPIService::AUTH_METHOD_OAUTH
);
// determine if the full reset is done when configuration is already with "oidc"
$runningFullResetWithOIDCAuth = (
$runningFullReset &&
$oldAuthMethod === OpenProjectAPIService::AUTH_METHOD_OIDC
);
if (
(key_exists('openproject_client_id', $values) && key_exists('openproject_client_secret', $values))
) {
$oldClientId = $this->config->getAppValue(
Application::APP_ID, 'openproject_client_id', ''
);
$oldClientSecret = $this->config->getAppValue(
Application::APP_ID, 'openproject_client_secret', ''
);
}
// since we can now switch between both authorization method we need to know what we are resetting (either "oauth2" or "oidc" method)
// determines if we are switching from "oauth2" to "oidc" auth method
$runningOauth2Reset = (
key_exists('openproject_client_id', $values) &&
!$values['openproject_client_id'] &&
key_exists('openproject_client_secret', $values) &&
!$values['openproject_client_secret'] &&
$oldOpenProjectOauthUrl &&
$oldClientId &&
$oldClientSecret
);
// determines if we are switching from "oidc" to "oauth2" auth method
$runningOIDCReset = false;
if (
key_exists('oidc_provider', $values) &&
key_exists('targeted_audience_client_id', $values)
) {
$oldOidcProvider = $this->config->getAppValue(
Application::APP_ID, 'oidc_provider', ''
);
$oldTargetedAudienceClient = $this->config->getAppValue(
Application::APP_ID, 'targeted_audience_client_id', ''
);
$runningOIDCReset = (
$oldOidcProvider &&
$oldTargetedAudienceClient &&
!$values['oidc_provider'] &&
!$values['targeted_audience_client_id']);
}
foreach ($values as $key => $value) {
if ($key === 'setup_project_folder' || $key === 'setup_app_password') {
continue;
@ -253,7 +335,7 @@ class ConfigController extends Controller {
// if the OpenProject OAuth URL has changed
if (key_exists('openproject_instance_url', $values)
&& $oldOpenProjectOauthUrl !== $values['openproject_instance_url']
&& $oldOpenProjectOauthUrl !== $values['openproject_instance_url'] && (!$runningOIDCReset || !$runningFullResetWithOIDCAuth)
) {
// delete the existing OAuth client if new OAuth URL is passed empty
if (
@ -272,22 +354,6 @@ class ConfigController extends Controller {
}
}
$runningFullReset = (
key_exists('openproject_instance_url', $values) &&
key_exists('openproject_client_id', $values) &&
key_exists('openproject_client_secret', $values) &&
$values['openproject_instance_url'] === null &&
$values['openproject_client_id'] === null &&
$values['openproject_client_secret'] === null
);
// resetting and keeping the project folder setup should delete the user app password
if (key_exists('setup_app_password', $values) && $values['setup_app_password'] === false) {
$this->openprojectAPIService->deleteAppPassword();
@ -301,10 +367,11 @@ class ConfigController extends Controller {
$this->config->deleteAppValue(Application::APP_ID, 'oPOAuthTokenRevokeStatus');
if (
// when the OP client information has changed
((key_exists('openproject_client_id', $values) && $values['openproject_client_id'] !== $oldClientId) ||
(key_exists('openproject_client_secret', $values) && $values['openproject_client_secret'] !== $oldClientSecret)) ||
// when the OP client information is for reset
$runningFullReset
(!$runningFullResetWithOIDCAuth && ((key_exists('openproject_client_id', $values) && $values['openproject_client_id'] !== $oldClientId) ||
(key_exists('openproject_client_secret', $values) && $values['openproject_client_secret'] !== $oldClientSecret))) ||
// when the OP client information is reset
$runningFullResetWithOAuth2Auth ||
$runningOauth2Reset
) {
$this->userManager->callForAllUsers(function (IUser $user) use (
$oldOpenProjectOauthUrl, $oldClientId, $oldClientSecret
@ -346,6 +413,8 @@ class ConfigController extends Controller {
}
$this->clearUserInfo($userUID);
});
} elseif ($runningFullResetWithOIDCAuth) {
$this->resetOIDCConfigs();
}
@ -362,6 +431,18 @@ class ConfigController extends Controller {
$this->config->setAppValue(Application::APP_ID, 'fresh_project_folder_setup', "0");
}
// when switching from "oauth2" to "oidc" authorization method
if (key_exists('authorization_method', $values) &&
$values['authorization_method'] === OpenProjectAPIService::AUTH_METHOD_OIDC && $runningOauth2Reset) {
$this->resetOauth2Configs();
}
// when switching from "oidc" to "oauth2" authorization method
if (key_exists('authorization_method', $values) &&
$values['authorization_method'] === OpenProjectAPIService::AUTH_METHOD_OAUTH && $runningOIDCReset) {
$this->resetOIDCConfigs();
}
// if the revoke has failed at least once, the last status is stored in the database
// this is not a neat way to give proper information about the revoke status
// TODO: find way to report every user's revoke status
@ -604,13 +685,12 @@ class ConfigController extends Controller {
* @return DataResponse
*/
public function checkAdminConfigOk(): DataResponse {
$adminConfigStatusWithoutGroupFolderSetupStatus = OpenProjectAPIService::isAdminConfigOk($this->config);
$appPasswordSetStatus = $this->openprojectAPIService->hasAppPassword();
// Admin config can be set in two parts
// 1. config without project folder set up (which is compulsory for integration)
// 2. config with project folder set up (which is optional for admin)
return new DataResponse([
'config_status_without_project_folder' => $adminConfigStatusWithoutGroupFolderSetupStatus,
'config_status_without_project_folder' => OpenProjectAPIService::isAdminConfigOk($this->config),
'project_folder_setup_status' => $appPasswordSetStatus
]);
}

Просмотреть файл

@ -87,6 +87,29 @@ class OpenProjectAPIController extends Controller {
$this->logger = $logger;
}
private function validatePreRequestConditions(): array {
$authMethod = $this->config->getAppValue(Application::APP_ID, 'authorization_method', '');
if ($authMethod === OpenProjectAPIService::AUTH_METHOD_OAUTH && $this->accessToken === '') {
return [
'status' => false,
'result' => new DataResponse('', Http::STATUS_UNAUTHORIZED)
];
} elseif ($authMethod === OpenProjectAPIService::AUTH_METHOD_OIDC &&
!$this->openprojectAPIService->getOIDCToken()
) {
return [
'status' => false,
'result' => new DataResponse('', Http::STATUS_UNAUTHORIZED)
];
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return [
'status' => false,
'result' => new DataResponse('', Http::STATUS_BAD_REQUEST)
];
}
return ['status' => true, 'result' => null];
}
/**
* get openproject instance URL
* @NoAdminRequired
@ -96,7 +119,6 @@ class OpenProjectAPIController extends Controller {
public function getOpenProjectUrl(): DataResponse {
return new DataResponse($this->openprojectUrl);
}
/**
* get openproject user avatar
* @NoAdminRequired
@ -127,10 +149,9 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getNotifications(): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
$result = $this->openprojectAPIService->getNotifications($this->userId);
if (!isset($result['error'])) {
@ -161,10 +182,9 @@ class OpenProjectAPIController extends Controller {
?int $fileId = null,
bool $isSmartPicker = false
): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
// when the search is done through smart picker we don't want to check if the work package is linkable
$result = $this->openprojectAPIService->searchWorkPackage(
@ -199,12 +219,10 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function linkWorkPackageToFile(array $values): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->linkWorkPackageToFile(
$values,
@ -228,10 +246,9 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function markNotificationAsRead(int $workpackageId) {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->markAllNotificationsOfWorkPackageAsRead(
@ -258,12 +275,10 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getWorkPackageFileLinks(int $id): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->getWorkPackageFileLinks(
$id,
@ -285,12 +300,10 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function deleteFileLink(int $id): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->deleteFileLink(
$id,
@ -316,12 +329,10 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getOpenProjectWorkPackageStatus(string $id): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
$result = $this->openprojectAPIService->getOpenProjectWorkPackageStatus(
$this->userId, $id
);
@ -343,12 +354,10 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getOpenProjectWorkPackageType(string $id): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
$result = $this->openprojectAPIService->getOpenProjectWorkPackageType(
$this->userId, $id
);
@ -366,10 +375,9 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getAvailableOpenProjectProjects(?string $searchQuery = null): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->getAvailableOpenProjectProjects($this->userId, $searchQuery);
@ -418,10 +426,9 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getOpenProjectWorkPackageForm(string $projectId, array $body): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->getOpenProjectWorkPackageForm($this->userId, $projectId, $body);
@ -440,10 +447,9 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getAvailableAssigneesOfAProject(string $projectId): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->getAvailableAssigneesOfAProject($this->userId, $projectId);
@ -489,10 +495,9 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function createWorkPackage(array $body): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
// we don't want to check if all the data in the body is set or not because
// that calculation will be done by the openproject api itself
@ -518,10 +523,9 @@ class OpenProjectAPIController extends Controller {
* @return DataResponse
*/
public function getOpenProjectConfiguration(): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
$validatePreRequestResult = $this->validatePreRequestConditions();
if (!$validatePreRequestResult['status']) {
return $validatePreRequestResult['result'];
}
try {
$result = $this->openprojectAPIService->getOpenProjectConfiguration($this->userId);

Просмотреть файл

@ -60,18 +60,26 @@ class OpenProjectWidget implements IWidget {
*/
private $user;
/**
* @var OpenProjectAPIService
*/
private OpenProjectAPIService $openProjectAPIService;
public function __construct(
IL10N $l10n,
IInitialState $initialStateService,
IURLGenerator $url,
IConfig $config,
IUserSession $userSession
IUserSession $userSession,
OpenProjectAPIService $openProjectAPIService
) {
$this->initialStateService = $initialStateService;
$this->l10n = $l10n;
$this->url = $url;
$this->config = $config;
$this->user = $userSession->getUser();
$this->openProjectAPIService = $openProjectAPIService;
}
/**
@ -115,15 +123,25 @@ class OpenProjectWidget implements IWidget {
public function load(): void {
Util::addScript(Application::APP_ID, Application::APP_ID . '-dashboard');
Util::addStyle(Application::APP_ID, 'dashboard');
$this->initialStateService->provideInitialState('admin-config-status', OpenProjectAPIService::isAdminConfigOk($this->config));
$oauthConnectionResult = $this->config->getUserValue(
$this->user->getUID(), Application::APP_ID, 'oauth_connection_result', ''
);
$this->config->deleteUserValue(
$this->user->getUID(), Application::APP_ID, 'oauth_connection_result'
);
$authorizationMethod = $this->config->getAppValue(Application::APP_ID, 'authorization_method', '');
$this->initialStateService->provideInitialState('authorization_method', $authorizationMethod);
$this->initialStateService->provideInitialState(
'admin_config_ok', OpenProjectAPIService::isAdminConfigOk($this->config)
);
// authorization method can be either a 'oidc' or 'oauth2'
// for 'oidc' state to be loaded
$token = $this->openProjectAPIService->getOIDCToken();
$this->initialStateService->provideInitialState('user-has-oidc-token', $token !== null);
// for 'oauth2' state to be loaded
$this->initialStateService->provideInitialState(
'oauth-connection-result', $oauthConnectionResult
);

Просмотреть файл

@ -0,0 +1,36 @@
<?php
/**
* Nextcloud - openproject
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Sagar Gurung
* @copyright Sagar Gurung 2024
*/
namespace OCA\OpenProject;
use OCA\OpenProject\AppInfo\Application;
use OCA\UserOIDC\Event\ExchangedTokenRequestedEvent;
use OCP\IConfig;
class ExchangedTokenRequestedEventHelper {
private IConfig $config;
public function __construct(
IConfig $config
) {
$this->config = $config;
}
/**
* @return ExchangedTokenRequestedEvent
*/
public function getEvent(): ExchangedTokenRequestedEvent {
return new ExchangedTokenRequestedEvent(
$this->config->getAppValue(Application::APP_ID, 'targeted_audience_client_id', '')
);
}
}

Просмотреть файл

@ -5,8 +5,10 @@ namespace OCA\OpenProject\Listener;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\OpenProject\AppInfo\Application;
use OCA\OpenProject\ServerVersionHelper;
use OCA\OpenProject\Service\OpenProjectAPIService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\Util;
/**
@ -14,7 +16,33 @@ use OCP\Util;
*/
class LoadAdditionalScriptsListener implements IEventListener {
/**
* @var OpenProjectAPIService
*/
private $openProjectAPIService;
/**
* @var IConfig
*/
private $config;
public function __construct(
IConfig $config,
OpenProjectAPIService $openProjectAPIService,
) {
$this->config = $config;
$this->openProjectAPIService = $openProjectAPIService;
}
public function handle(Event $event): void {
// When user is non oidc based or there is some error when getting token for the targeted client
// then we need to hide the oidc based connection for the user
// so this check is required
if (
$this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === OpenProjectAPIService::AUTH_METHOD_OIDC &&
!$this->openProjectAPIService->getOIDCToken()
) {
return;
}
if (!$event instanceof LoadAdditionalScriptsEvent) {
return;
}

Просмотреть файл

@ -66,16 +66,30 @@ class LoadSidebarScript implements IEventListener {
*/
protected $appManager;
/**
* @var OpenProjectAPIService
*/
private $openProjectAPIService;
private IUserSession $userSession;
/**
* @var string|null
*/
private $userId;
public function __construct(
IInitialState $initialStateService,
IConfig $config,
IUserSession $userSession,
IAppManager $appManager
IAppManager $appManager,
OpenProjectAPIService $openProjectAPIService,
?string $userId
) {
$this->initialStateService = $initialStateService;
$this->config = $config;
$this->appManager = $appManager;
$this->userId = $userId;
$user = $userSession->getUser();
$this->openProjectAPIService = $openProjectAPIService;
if (strpos(\OC::$server->get(IRequest::class)->getRequestUri(), 'files') !== false) {
$this->oauthConnectionResult = $this->config->getUserValue(
$user->getUID(), Application::APP_ID, 'oauth_connection_result', ''
@ -93,6 +107,15 @@ class LoadSidebarScript implements IEventListener {
}
public function handle(Event $event): void {
// When user is non oidc based or there is some error when getting token for the targeted client
// then we need to hide the oidc based connection for the user
// so this check is required
if (
$this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === OpenProjectAPIService::AUTH_METHOD_OIDC &&
!$this->openProjectAPIService->getOIDCToken()
) {
return;
}
if (!($event instanceof LoadSidebar)) {
return;
}
@ -108,17 +131,25 @@ class LoadSidebarScript implements IEventListener {
}
Util::addStyle(Application::APP_ID, 'tab');
$this->initialStateService->provideInitialState('admin-config-status', OpenProjectAPIService::isAdminConfigOk($this->config));
$authorizationMethod = $this->config->getAppValue(Application::APP_ID, 'authorization_method', '');
$this->initialStateService->provideInitialState('authorization_method', $authorizationMethod);
$this->initialStateService->provideInitialState(
'openproject-url', $this->config->getAppValue(Application::APP_ID, 'openproject_instance_url')
);
$this->initialStateService->provideInitialState(
'admin_config_ok', OpenProjectAPIService::isAdminConfigOk($this->config)
);
// authorization method can be either a 'oidc' or 'oauth2'
// for 'oidc' the user info needs to be set (once token has been exchanged)
$this->openProjectAPIService->setUserInfoForOidcBasedAuth($this->userId);
// for 'oauth2' state to be loaded
$this->initialStateService->provideInitialState(
'oauth-connection-result', $this->oauthConnectionResult
);
$this->initialStateService->provideInitialState(
'oauth-connection-error-message', $this->oauthConnectionErrorMessage
);
$this->initialStateService->provideInitialState(
'openproject-url',
$this->config->getAppValue(Application::APP_ID, 'openproject_instance_url')
);
}
}

Просмотреть файл

@ -46,22 +46,42 @@ class OpenProjectReferenceListener implements IEventListener {
* @var IConfig
*/
private $config;
/**
* @var OpenProjectAPIService
*/
private $openProjectAPIService;
public function __construct(
IInitialState $initialStateService,
IConfig $config
IConfig $config,
OpenProjectAPIService $openProjectAPIService,
) {
$this->initialStateService = $initialStateService;
$this->config = $config;
$this->openProjectAPIService = $openProjectAPIService;
}
public function handle(Event $event): void {
// When user is non oidc based or there is some error when getting token for the targeted client
// then we need to hide the oidc based connection for the user
// so this check is required
if (
$this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === OpenProjectAPIService::AUTH_METHOD_OIDC &&
!$this->openProjectAPIService->getOIDCToken()
) {
return;
}
if (!$event instanceof RenderReferenceEvent) {
return;
}
Util::addScript(Application::APP_ID, Application::APP_ID . '-reference');
$this->initialStateService->provideInitialState('admin-config-status', OpenProjectAPIService::isAdminConfigOk($this->config));
$adminConfig = [
'isAdminConfigOk' => OpenProjectAPIService::isAdminConfigOk($this->config),
'authMethod' => $this->config->getAppValue(Application::APP_ID, 'authorization_method', '')
];
$this->initialStateService->provideInitialState(
'admin-config',
$adminConfig
);
$this->initialStateService->provideInitialState(
'openproject-url',
$this->config->getAppValue(Application::APP_ID, 'openproject_instance_url')

Просмотреть файл

@ -109,13 +109,23 @@ class OpenProjectSearchProvider implements IProvider {
$offset = $query->getCursor();
$offset = $offset ? intval($offset) : 0;
$openprojectUrl = OpenProjectAPIService::sanitizeUrl($this->config->getAppValue(Application::APP_ID, 'openproject_instance_url'));
$accessToken = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'token');
$authorizationMethod = $this->config->getAppValue(Application::APP_ID, 'authorization_method', '');
$searchEnabled = $this->config->getUserValue(
$user->getUID(),
Application::APP_ID, 'search_enabled',
$this->config->getAppValue(Application::APP_ID, 'default_enable_unified_search', '0')) === '1';
if ($accessToken === '' || !$searchEnabled) {
return SearchResult::paginated($this->getName(), [], 0);
if ($authorizationMethod === OpenProjectAPIService::AUTH_METHOD_OAUTH) {
$accessToken = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'token');
if (!$accessToken || !$searchEnabled) {
return SearchResult::paginated($this->getName(), [], 0);
}
} elseif ($authorizationMethod === OpenProjectAPIService::AUTH_METHOD_OIDC) {
$accessToken = $this->service->getOIDCToken();
if (!$accessToken || !$searchEnabled) {
return SearchResult::paginated($this->getName(), [], 0);
}
}
$searchResults = $this->service->searchWorkPackage($user->getUID(), $term, null, false);

Просмотреть файл

@ -29,9 +29,12 @@ use OCA\OpenProject\Exception\OpenprojectAvatarErrorException;
use OCA\OpenProject\Exception\OpenprojectErrorException;
use OCA\OpenProject\Exception\OpenprojectGroupfolderSetupConflictException;
use OCA\OpenProject\Exception\OpenprojectResponseException;
use OCA\OpenProject\ExchangedTokenRequestedEventHelper;
use OCA\TermsOfService\Db\Entities\Signatory;
use OCA\TermsOfService\Db\Mapper\SignatoryMapper;
use OCA\TermsOfService\Db\Mapper\TermsMapper;
use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Exception\TokenExchangeFailedException;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\Encryption\IManager;
@ -62,6 +65,8 @@ use Psr\Log\LoggerInterface;
define('CACHE_TTL', 3600);
class OpenProjectAPIService {
public const AUTH_METHOD_OAUTH = 'oauth2';
public const AUTH_METHOD_OIDC = 'oidc';
/**
* @var string
*/
@ -133,6 +138,7 @@ class OpenProjectAPIService {
private ISecureRandom $random;
private IEventDispatcher $eventDispatcher;
private AuditLogger $auditLogger;
private ExchangedTokenRequestedEventHelper $exchangedTokenRequestedEventHelper;
public function __construct(
string $appName,
@ -153,7 +159,8 @@ class OpenProjectAPIService {
ISubAdmin $subAdminManager,
IDBConnection $db,
ILogFactory $logFactory,
IManager $encryptionManager
IManager $encryptionManager,
ExchangedTokenRequestedEventHelper $exchangedTokenRequestedEventHelper,
) {
$this->appName = $appName;
$this->avatarManager = $avatarManager;
@ -174,6 +181,7 @@ class OpenProjectAPIService {
$this->db = $db;
$this->logFactory = $logFactory;
$this->encryptionManager = $encryptionManager;
$this->exchangedTokenRequestedEventHelper = $exchangedTokenRequestedEventHelper;
}
/**
@ -316,9 +324,13 @@ class OpenProjectAPIService {
string $openprojectUserName,
string $nextcloudUserId
): array {
$accessToken = $this->config->getUserValue($nextcloudUserId, Application::APP_ID, 'token');
$this->config->getAppValue(Application::APP_ID, 'openproject_client_id');
$this->config->getAppValue(Application::APP_ID, 'openproject_client_secret');
if ($this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === self::AUTH_METHOD_OIDC) {
$accessToken = $this->getOIDCToken();
} else {
$accessToken = $this->config->getUserValue($nextcloudUserId, Application::APP_ID, 'token');
$this->config->getAppValue(Application::APP_ID, 'openproject_client_id');
$this->config->getAppValue(Application::APP_ID, 'openproject_client_secret');
}
$openprojectUrl = $this->config->getAppValue(Application::APP_ID, 'openproject_instance_url');
try {
$response = $this->rawRequest(
@ -439,10 +451,14 @@ class OpenProjectAPIService {
*/
public function request(string $userId,
string $endPoint, array $params = [], string $method = 'GET'): array {
$accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token');
$refreshToken = $this->config->getUserValue($userId, Application::APP_ID, 'refresh_token');
$clientID = $this->config->getAppValue(Application::APP_ID, 'openproject_client_id');
$clientSecret = $this->config->getAppValue(Application::APP_ID, 'openproject_client_secret');
if ($this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === self::AUTH_METHOD_OIDC) {
$accessToken = $this->getOIDCToken();
} else {
$accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token');
$refreshToken = $this->config->getUserValue($userId, Application::APP_ID, 'refresh_token');
$clientID = $this->config->getAppValue(Application::APP_ID, 'openproject_client_id');
$clientSecret = $this->config->getAppValue(Application::APP_ID, 'openproject_client_secret');
}
$openprojectUrl = $this->config->getAppValue(Application::APP_ID, 'openproject_instance_url');
if (!$openprojectUrl || !OpenProjectAPIService::validateURL($openprojectUrl)) {
return ['error' => 'OpenProject URL is invalid', 'statusCode' => 500];
@ -460,7 +476,11 @@ class OpenProjectAPIService {
$body = (string) $response->getBody();
// refresh token if it's invalid and we are using oauth
// response can be : 'OAuth2 token is expired!', 'Invalid token!' or 'Not authorized'
if ($response->getStatusCode() === 401) {
// This condition applies exclusively to the OAuth2 authorization method and not to OIDC authorization,
// as token refreshing for OIDC is managed by the 'user_oidc' application.
if ($response->getStatusCode() === 401 &&
$this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === self::AUTH_METHOD_OAUTH
) {
$this->logger->info('Trying to REFRESH the access token', ['app' => $this->appName]);
// try to refresh the token
$result = $this->requestOAuthAccessToken($openprojectUrl, [
@ -912,13 +932,13 @@ class OpenProjectAPIService {
}
/**
* checks if every admin config variables are set
* checks if every admin config for oauth2 based authorization variables are set
* checks if the oauth instance url is valid
*
* @param IConfig $config
* @return bool
*/
public static function isAdminConfigOk(IConfig $config):bool {
public static function isAdminConfigOkForOauth2(IConfig $config):bool {
$clientId = $config->getAppValue(Application::APP_ID, 'openproject_client_id');
$clientSecret = $config->getAppValue(Application::APP_ID, 'openproject_client_secret');
$oauthInstanceUrl = $config->getAppValue(Application::APP_ID, 'openproject_instance_url');
@ -930,6 +950,41 @@ class OpenProjectAPIService {
}
}
/**
* checks if every admin config for oidc based authorization variables are set
* checks if the oauth instance url is valid
*
* @param IConfig $config
* @return bool
*/
public static function isAdminConfigOkForOIDCAuth(IConfig $config):bool {
$oidcProvider = $config->getAppValue(Application::APP_ID, 'oidc_provider');
$targetAudienceClientId = $config->getAppValue(Application::APP_ID, 'targeted_audience_client_id');
$oauthInstanceUrl = $config->getAppValue(Application::APP_ID, 'openproject_instance_url');
$checkIfConfigIsSet = !!($oidcProvider) && !!($targetAudienceClientId) && !!($oauthInstanceUrl);
if (!$checkIfConfigIsSet) {
return false;
} else {
return self::validateURL($oauthInstanceUrl);
}
}
/**
* returns overall admin config status whether it be 'oidc' or 'oauth2'
*
* @return bool
*/
public static function isAdminConfigOk(IConfig $config): bool {
$authMethod = $config->getAppValue(Application::APP_ID, 'authorization_method');
if ($authMethod === self::AUTH_METHOD_OAUTH) {
return self::isAdminConfigOkForOauth2($config);
} elseif ($authMethod === self::AUTH_METHOD_OIDC) {
return self::isAdminConfigOkForOIDCAuth($config);
}
return false;
}
/**
* makes sure the URL has no extra slashes
*/
@ -1339,7 +1394,11 @@ class OpenProjectAPIService {
* @return array<mixed>|null
*/
public function getWorkPackageInfo(string $userId, int $wpId): ?array {
$accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token');
if ($this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === self::AUTH_METHOD_OIDC) {
$accessToken = $this->getOIDCToken();
} else {
$accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token');
}
if ($accessToken) {
$searchResult = $this->searchWorkPackage($userId, null, null, false, $wpId);
if (isset($searchResult['error'])) {
@ -1558,4 +1617,69 @@ class OpenProjectAPIService {
}
return $result;
}
/**
* @return string|null
*/
public function getOIDCToken(): ?string {
if (!$this->isUserOIDCAppInstalledAndEnabled()) {
$this->logger->debug('The user_oidc app is not installed or enabled');
return null;
}
try {
$event = $this->exchangedTokenRequestedEventHelper->getEvent();
/** @psalm-suppress InvalidArgument for dispatchTyped($event)
* but new ExchangedTokenRequestedEvent(targeted_audience_client_id) returns event
*/
$this->eventDispatcher->dispatchTyped($event);
} catch (TokenExchangeFailedException $e) {
$this->logger->debug('Failed to exchange token: ' . $e->getMessage());
return null;
}
$token = $event->getToken();
if ($token === null) {
$this->logger->debug('ExchangedTokenRequestedEvent event has not been caught by user_oidc');
return null;
}
// token expiration info
$this->logger->debug('Obtained a token that expires in ' . $token->getExpiresInFromNow());
return $token->getAccessToken();
}
/**
* @param string $userId
* @return void
*/
public function setUserInfoForOidcBasedAuth(string $userId): void {
$info = $this->request($userId, 'users/me');
if (isset($info['lastName'], $info['firstName'], $info['id'])) {
$fullName = $info['firstName'] . ' ' . $info['lastName'];
$this->config->setUserValue($userId, Application::APP_ID, 'user_id', $info['id']);
$this->config->setUserValue($userId, Application::APP_ID, 'user_name', $fullName);
}
}
public function getRegisteredOidcProviders(): array {
$oidcProviders = [];
if ($this->isUserOIDCAppInstalledAndEnabled()) {
$providerMapper = new ProviderMapper($this->db);
foreach ($providerMapper->getProviders() as $provider) {
$oidcProviders[] = $provider->getIdentifier();
}
}
return $oidcProviders;
}
public function isUserOIDCAppInstalledAndEnabled(): bool {
return (
class_exists('\OCA\UserOIDC\Db\ProviderMapper') &&
class_exists('\OCA\UserOIDC\Event\ExchangedTokenRequestedEvent') &&
class_exists('\OCA\UserOIDC\Exception\TokenExchangeFailedException') &&
$this->appManager->isInstalled(
'user_oidc',
)
);
}
}

Просмотреть файл

@ -34,7 +34,8 @@ class Admin implements ISettings {
public function __construct(IConfig $config,
OauthService $oauthService,
OpenProjectAPIService $openProjectAPIService,
IInitialState $initialStateService) {
IInitialState $initialStateService
) {
$this->config = $config;
$this->initialStateService = $initialStateService;
$this->oauthService = $oauthService;
@ -59,10 +60,18 @@ class Admin implements ISettings {
$projectFolderStatusInformation = $this->openProjectAPIService->getProjectFolderSetupInformation();
$isAllTermsOfServiceSignedForUserOpenProject = $this->openProjectAPIService->isAllTermsOfServiceSignedForUserOpenProject();
$isAdminAuditConfigurationSetUpCorrectly = $this->openProjectAPIService->isAdminAuditConfigSetCorrectly();
$adminConfig = [
'openproject_client_id' => $clientID,
'openproject_client_secret' => $clientSecret,
'openproject_instance_url' => $oauthUrl,
'authorization_method' => $this->config->getAppValue(Application::APP_ID, 'authorization_method', ''),
'authorization_settings' => [
'oidc_provider' => $this->config->getAppValue(Application::APP_ID, 'oidc_provider', ''),
'targeted_audience_client_id' => $this->config->getAppValue(
Application::APP_ID, 'targeted_audience_client_id', ''
),
],
'nc_oauth_client' => $clientInfo,
'default_enable_navigation' => $this->config->getAppValue(Application::APP_ID, 'default_enable_navigation', '0') === '1',
'default_enable_unified_search' => $this->config->getAppValue(Application::APP_ID, 'default_enable_unified_search', '0') === '1',
@ -74,13 +83,16 @@ class Admin implements ISettings {
'encryption_info' => [
'server_side_encryption_enabled' => $this->openProjectAPIService->isServerSideEncryptionEnabled(),
'encryption_enabled_for_groupfolders' => $this->config->getAppValue('groupfolders', 'enable_encryption', '') === 'true'
]
],
'oidc_provider' => $this->openProjectAPIService->getRegisteredOidcProviders(),
'user_oidc_enabled' => $this->openProjectAPIService->isUserOIDCAppInstalledAndEnabled()
];
$adminConfigStatus = OpenProjectAPIService::isAdminConfigOk($this->config);
$this->initialStateService->provideInitialState('admin-config', $adminConfig);
$this->initialStateService->provideInitialState('admin-config-status', $adminConfigStatus);
$this->initialStateService->provideInitialState(
'admin-config-status', OpenProjectAPIService::isAdminConfigOk($this->config)
);
return new TemplateResponse(Application::APP_ID, 'adminSettings');
}

Просмотреть файл

@ -25,24 +25,40 @@ class Personal implements ISettings {
*/
private $userId;
/**
* @var OpenProjectAPIService
*/
private $openProjectAPIService;
public function __construct(
IConfig $config,
IInitialState $initialStateService,
OpenProjectAPIService $openProjectAPIService,
?string $userId) {
$this->config = $config;
$this->initialStateService = $initialStateService;
$this->userId = $userId;
$this->openProjectAPIService = $openProjectAPIService;
}
/**
* @return TemplateResponse
*/
public function getForm(): TemplateResponse {
$token = $this->config->getUserValue($this->userId, Application::APP_ID, 'token');
$authorizationMethod = $this->config->getAppValue(Application::APP_ID, 'authorization_method', '');
$token = '';
if ($authorizationMethod === OpenProjectAPIService::AUTH_METHOD_OIDC) {
$token = $this->openProjectAPIService->getOIDCToken();
if ($token) {
// when connection is oidc based then user information needs to be saved
$this->openProjectAPIService->setUserInfoForOidcBasedAuth($this->userId);
}
}
if ($authorizationMethod === OpenProjectAPIService::AUTH_METHOD_OAUTH) {
$token = $this->config->getUserValue($this->userId, Application::APP_ID, 'token');
}
$userName = $this->config->getUserValue($this->userId, Application::APP_ID, 'user_name');
// take the fallback value from the defaults
$searchEnabled = $this->config->getUserValue(
$this->userId,
@ -65,6 +81,7 @@ class Personal implements ISettings {
];
$userConfig['admin_config_ok'] = OpenProjectAPIService::isAdminConfigOk($this->config);
$userConfig['authorization_method'] = $authorizationMethod;
$this->initialStateService->provideInitialState('user-config', $userConfig);
$oauthConnectionResult = $this->config->getUserValue(

Просмотреть файл

@ -31,6 +31,11 @@
<referencedClass name="OCA\TermsOfService\Db\Mapper\SignatoryMapper" />
<referencedClass name="OCA\TermsOfService\Db\Entities\Signatory" />
<referencedClass name="OCA\TermsOfService\Db\Mapper\TermsMapper" />
<!-- these classes belong to the user_oidc app, which isn't compulsory, so might not exist while running psalm -->
<referencedClass name="OCA\UserOIDC\Db\ProviderMapper" />
<referencedClass name="OCA\UserOIDC\Model\Token" />
<referencedClass name="OCA\UserOIDC\Event\ExchangedTokenRequestedEvent" />
<referencedClass name="OCA\UserOIDC\Exception\TokenExchangeFailedException" />
<!-- these classes belong to the activity app, which isn't compulsory, so might not exist while running psalm -->
<referencedClass name="OCA\Activity\UserSettings" />
<referencedClass name="OCA\Activity\GroupHelperDisabled" />
@ -83,6 +88,9 @@
<referencedClass name="OC\Http\Client\LocalAddressChecker"/>
<!-- these are classes form terms_of_service app, which isn't cloned while doing static code analysis -->
<referencedClass name="OCA\TermsOfService\Db\Mapper\SignatoryMapper" />
<!-- these classes belong to the user_oidc app, which isn't compulsory, so might not exist while running psalm -->
<referencedClass name="OCA\UserOIDC\Model\Token" />
<referencedClass name="OCA\UserOIDC\Event\ExchangedTokenRequestedEvent" />
<!-- these are classes form activity app, which isn't cloned while doing static code analysis -->
<referencedClass name="OCA\Activity\UserSettings" />
<referencedClass name="OCA\Activity\GroupHelperDisabled" />

Просмотреть файл

@ -57,13 +57,153 @@
</NcButton>
</div>
</div>
<div class="openproject-oauth-values">
<div class="authorization-method">
<FormHeading index="2"
:title="t('integration_openproject', 'Authorization method')"
:is-complete="isAuthorizationMethodFormComplete"
:is-disabled="isAuthorizationFormInDisabledMode"
:is-dark-theme="isDarkTheme" />
<div v-if="isServerHostFormComplete">
<div v-if="isAuthorizationFormInEditMode" class="authorization-method">
<div class="authorization-method--description">
<p class="title">
{{ t('integration_openproject', 'Need help setting this up?') }}
</p>
<p class="description" v-html="getAuthorizationMethodHintText" /> <!-- eslint-disable-line vue/no-v-html -->
</div>
<div class="authorization-method--options">
<NcCheckboxRadioSwitch class="radio-check"
:checked.sync="authorizationMethod.authorizationMethodSet"
:value="authMethods.OAUTH2"
type="radio">
{{ authMethodsLabel.OAUTH2 }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch class="radio-check"
:checked.sync="authorizationMethod.authorizationMethodSet"
:value="authMethods.OIDC"
:disabled="!isOIDCAppInstalledAndEnabled"
type="radio">
{{ authMethodsLabel.OIDC }}
</NcCheckboxRadioSwitch>
<p v-if="!isOIDCAppInstalledAndEnabled" class="oidc-app-check-description" v-html="getOIDCAppNotInstalledHintText" /> <!-- eslint-disable-line vue/no-v-html -->
</div>
</div>
<div v-else>
<p class="title">
{{ getSelectedAuthenticatedMethod }}
</p>
</div>
<div class="form-actions">
<NcButton v-if="isAuthorizationMethodFormInViewMode"
data-test-id="reset-authorization-method-btn"
@click="setAuthorizationMethodInEditMode">
<template #icon>
<PencilIcon :size="20" />
</template>
{{ t('integration_openproject', 'Edit authorization method') }}
</NcButton>
<NcButton v-if="isAuthorizationFormInEditMode && authorizationMethod.currentAuthorizationMethodSelected !== null"
class="mr-2"
data-test-id="cancel-edit-auth-method-btn"
@click="setAuthorizationMethodToViewMode">
{{ t('integration_openproject', 'Cancel') }}
</NcButton>
<NcButton v-if="isAuthorizationFormInEditMode"
data-test-id="submit-auth-method-values-btn"
type="primary"
:disabled="isAuthorizationMethodSelected"
@click="selectAuthorizationMethod">
<template #icon>
<NcLoadingIcon v-if="loadingAuthorizationMethodForm" class="loading-spinner" :size="20" />
<CheckBoldIcon v-else fill-color="#FFFFFF" :size="20" />
</template>
{{ t('integration_openproject', 'Save') }}
</NcButton>
</div>
</div>
</div>
<div v-if="authorizationMethod.currentAuthorizationMethodSelected === authMethods.OIDC" class="authorization-settings">
<FormHeading index="3"
:title="t('integration_openproject', 'Authorization settings')"
:is-complete="isAuthorizationSettingFormComplete"
:is-disabled="isAuthorizationSettingFormInDisabledMode"
:is-dark-theme="isDarkTheme" />
<div class="authorization-settings--content">
<FieldValue v-if="isAuthorizationSettingsInViewMode"
is-required
class="pb-1"
:title="t('integration_openproject', 'OIDC Provider')"
:value="authorizationSetting.oidcProviderSet" />
<div v-else class="authorization-settings--content--provider">
<p class="authorization-settings--content--label">
{{ t('integration_openproject', 'OIDC provider *') }}
</p>
<div id="select">
<NcSelect
input-id="provider-search-input"
:placeholder="t('integration_openproject', 'Select an OIDC provider')"
:options="registeredOidcProviders"
:value="getCurrentSelectedOIDCProvider"
:filterable="true"
:close-on-select="true"
:clear-search-on-blur="() => false"
:append-to-body="false"
:label-outside="true"
:input-label="t('integration_openproject', 'OIDC provider')"
@option:selected="onSelectOIDCProvider" />
</div>
<p class="description" v-html="getConfigureOIDCHintText" /> <!-- eslint-disable-line vue/no-v-html -->
</div>
<FieldValue v-if="isAuthorizationSettingsInViewMode"
is-required
class="pb-1"
:title="t('integration_openproject', 'OpenProject client ID')"
:value="state.authorization_settings.targeted_audience_client_id" />
<div v-else class="authorization-settings--content--client">
<TextInput
id="authorization-method-target-client-id"
v-model="state.authorization_settings.targeted_audience_client_id"
class="py-1"
is-required
:label="t('integration_openproject', 'OpenProject client ID')"
hint-text="You can get this value from Keycloak when you set-up define the client" />
</div>
</div>
<div class="form-actions">
<NcButton v-if="isAuthorizationSettingsInViewMode"
data-test-id="reset-auth-settings-btn"
@click="setAuthorizationSettingInEditMode">
<template #icon>
<PencilIcon :size="20" />
</template>
{{ t('integration_openproject', 'Edit athorization settings') }}
</NcButton>
<NcButton v-if="isAuthorizationSettingInEditMode && authorizationSetting.currentOIDCProviderSelected !== null && authorizationSetting.targetedAudienceClientIdSet !== null"
class="mr-2"
data-test-id="cancel-edit-auth-setting-btn"
@click="setAuthorizationSettingToViewMode">
{{ t('integration_openproject', 'Cancel') }}
</NcButton>
<NcButton v-if="isAuthorizationSettingInEditMode"
data-test-id="submit-oidc-auth-settings-values-btn"
type="primary"
:disabled="isAuthorizationSettingsSelected"
@click="saveOIDCAuthSetting">
<template #icon>
<NcLoadingIcon v-if="loadingAuthorizationMethodForm" class="loading-spinner" :size="20" />
<CheckBoldIcon v-else fill-color="#FFFFFF" :size="20" />
</template>
{{ t('integration_openproject', 'Save') }}
</NcButton>
</div>
</div>
<div v-if="authorizationMethod.currentAuthorizationMethodSelected === authMethods.OAUTH2 || authorizationMethod.currentAuthorizationMethodSelected === null" class="openproject-oauth-values">
<FormHeading index="3"
:title="t('integration_openproject', 'OpenProject OAuth settings')"
:is-complete="isOPOAuthFormComplete"
:is-disabled="isOPOAuthFormInDisableMode"
:is-dark-theme="isDarkTheme" />
<div v-if="isServerHostFormComplete">
<div v-if="authorizationMethod.currentAuthorizationMethodSelected !== null">
<FieldValue v-if="isOPOAuthFormInView"
is-required
:value="state.openproject_client_id"
@ -111,8 +251,8 @@
</div>
</div>
</div>
<div class="nextcloud-oauth-values">
<FormHeading index="3"
<div v-if="authorizationMethod.currentAuthorizationMethodSelected === authMethods.OAUTH2 || authorizationMethod.currentAuthorizationMethodSelected === null" class="nextcloud-oauth-values">
<FormHeading index="4"
:title="t('integration_openproject', 'Nextcloud OAuth client')"
:is-complete="isNcOAuthFormComplete"
:is-disabled="isNcOAuthFormInDisableMode"
@ -177,7 +317,7 @@
</div>
</div>
<div class="project-folder-setup">
<FormHeading index="4"
<FormHeading :index="authorizationMethod.currentAuthorizationMethodSelected === authMethods.OIDC ? '4' : '5'"
:is-project-folder-setup-heading="true"
:title="t('integration_openproject', 'Project folders (recommended)')"
:is-setup-complete-without-project-folders="isSetupCompleteWithoutProjectFolders"
@ -286,7 +426,7 @@
</div>
</div>
<div v-if="state.app_password_set">
<FormHeading index="5"
<FormHeading index="6"
:title="t('integration_openproject', 'Project folders application connection')"
:is-complete="isOPUserAppPasswordFormComplete"
:is-disabled="isOPUserAppPasswordInDisableMode"
@ -338,7 +478,7 @@
</template>
{{ t('integration_openproject', 'Reset') }}
</NcButton>
<div v-if="isIntegrationComplete" class="default-prefs">
<div v-if="isIntegrationCompleteWithOauth2 || isIntegrationCompleteWithOIDC" class="default-prefs">
<h2>{{ t('integration_openproject', 'Default user settings') }}</h2>
<p>
{{ t('integration_openproject', 'A new user will receive these defaults and they will be applied to the integration app till the user changes them.') }}
@ -372,7 +512,13 @@ import { generateUrl } from '@nextcloud/router'
import { showSuccess, showError } from '@nextcloud/dialogs'
import CheckBoldIcon from 'vue-material-design-icons/CheckBold.vue'
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
import { NcLoadingIcon, NcCheckboxRadioSwitch, NcButton, NcNoteCard } from '@nextcloud/vue'
import {
NcLoadingIcon,
NcCheckboxRadioSwitch,
NcButton,
NcNoteCard,
NcSelect,
} from '@nextcloud/vue'
import RestoreIcon from 'vue-material-design-icons/Restore.vue'
import AutoRenewIcon from 'vue-material-design-icons/Autorenew.vue'
import TextInput from './admin/TextInput.vue'
@ -380,12 +526,13 @@ import FieldValue from './admin/FieldValue.vue'
import FormHeading from './admin/FormHeading.vue'
import CheckBox from '../components/settings/CheckBox.vue'
import SettingsTitle from '../components/settings/SettingsTitle.vue'
import { F_MODES, FORM, USER_SETTINGS } from '../utils.js'
import { F_MODES, FORM, USER_SETTINGS, AUTH_METHOD, AUTH_METHOD_LABEL } from '../utils.js'
import TermsOfServiceUnsigned from './admin/TermsOfServiceUnsigned.vue'
import dompurify from 'dompurify'
export default {
name: 'AdminSettings',
components: {
NcSelect,
NcButton,
FieldValue,
FormHeading,
@ -407,13 +554,15 @@ export default {
// server host form is never disabled.
// it's either editable or view only
server: F_MODES.EDIT,
authorizationMethod: F_MODES.DISABLE,
authorizationSetting: F_MODES.DISABLE,
opOauth: F_MODES.DISABLE,
ncOauth: F_MODES.DISABLE,
opUserAppPassword: F_MODES.DISABLE,
projectFolderSetUp: F_MODES.DISABLE,
},
isFormCompleted: {
server: false, opOauth: false, ncOauth: false, opUserAppPassword: false, projectFolderSetUp: false,
server: false, authorizationMethod: false, authorizationSetting: false, opOauth: false, ncOauth: false, opUserAppPassword: false, projectFolderSetUp: false,
},
buttonTextLabel: {
keepCurrentChange: t('integration_openproject', 'Keep current setup'),
@ -423,6 +572,8 @@ export default {
loadingServerHostForm: false,
loadingProjectFolderSetup: false,
loadingOPOauthForm: false,
loadingAuthorizationMethodForm: false,
loadingAuthorizationSettingForm: false,
isOpenProjectInstanceValid: null,
openProjectNotReachableErrorMessage: null,
openProjectNotReachableErrorMessageDetails: null,
@ -446,6 +597,22 @@ export default {
isDarkTheme: null,
isAllTermsOfServiceSignedForUserOpenProject: true,
userSettingDescription: USER_SETTINGS,
authMethods: AUTH_METHOD,
authMethodsLabel: AUTH_METHOD_LABEL,
// here 'Set' defines that the method is selected and saved in database (e.g authorizationMethodSet)
// whereas 'Selected' defines that it is the current selection (e.g currentAuthorizationMethodSelected)
authorizationMethod: {
// default authorization method is set to 'oauth2'
authorizationMethodSet: AUTH_METHOD.OAUTH2,
currentAuthorizationMethodSelected: null,
},
authorizationSetting: {
oidcProviderSet: null,
targetedAudienceClientIdSet: null,
currentOIDCProviderSelected: null,
currentTargetedAudienceClientIdSelected: null,
},
registeredOidcProviders: [],
}
},
computed: {
@ -473,6 +640,12 @@ export default {
isServerHostFormComplete() {
return this.isFormCompleted.server
},
isAuthorizationMethodFormComplete() {
return this.isFormCompleted.authorizationMethod
},
isAuthorizationSettingFormComplete() {
return this.isFormCompleted.authorizationSetting
},
isOPOAuthFormComplete() {
return this.isFormCompleted.opOauth
},
@ -488,6 +661,12 @@ export default {
isServerHostFormInView() {
return this.formMode.server === F_MODES.VIEW
},
isAuthorizationMethodFormInViewMode() {
return this.formMode.authorizationMethod === F_MODES.VIEW
},
isAuthorizationSettingsInViewMode() {
return this.formMode.authorizationSetting === F_MODES.VIEW
},
isOPOAuthFormInView() {
return this.formMode.opOauth === F_MODES.VIEW
},
@ -500,12 +679,24 @@ export default {
isOPOAuthFormInDisableMode() {
return this.formMode.opOauth === F_MODES.DISABLE
},
isAuthorizationFormInDisabledMode() {
return this.formMode.authorizationMethod === F_MODES.DISABLE
},
isAuthorizationSettingFormInDisabledMode() {
return this.formMode.authorizationSetting === F_MODES.DISABLE
},
isOPUserAppPasswordFormInEdit() {
return this.formMode.opUserAppPassword === F_MODES.EDIT
},
isProjectFolderSetupFormInEdit() {
return this.formMode.projectFolderSetUp === F_MODES.EDIT
},
isAuthorizationFormInEditMode() {
return this.formMode.authorizationMethod === F_MODES.EDIT
},
isAuthorizationSettingInEditMode() {
return this.formMode.authorizationSetting === F_MODES.EDIT
},
isNcOAuthFormInDisableMode() {
return this.formMode.ncOauth === F_MODES.DISABLE
},
@ -563,14 +754,39 @@ export default {
const htmlLink = `<a class="link" href="https://www.openproject.org/docs/system-admin-guide/integrations/nextcloud/#files-are-not-encrypted-when-using-nextcloud-server-side-encryption" target="_blank" title="${linkText}">${linkText}</a>`
return t('integration_openproject', 'Server-side encryption is active, but encryption for Group Folders is not yet enabled. To ensure secure storage of files in project folders, please follow the configuration steps in the {htmlLink}.', { htmlLink }, null, { escape: false, sanitize: false })
},
isIntegrationComplete() {
getAuthorizationMethodHintText() {
const linkText = t('integration_openproject', 'authorization methods you can use with OpenProject')
const htmlLink = `<a class="link" href="https://www.openproject.org/docs/system-admin-guide/integrations/nextcloud/#files-are-not-encrypted-when-using-nextcloud-server-side-encryption" target="_blank" title="${linkText}">${linkText}</a>`
return t('integration_openproject', 'Please read our guide on {htmlLink}.', { htmlLink }, null, { escape: false, sanitize: false })
},
getOIDCAppNotInstalledHintText() {
const linkText = t('integration_openproject', 'User OIDC')
const url = generateUrl('settings/apps/files/user_oidc')
const htmlLink = `<a class="link" href="${url}" target="_blank" title="${linkText}">${linkText}</a>`
return t('integration_openproject', 'Please install the {htmlLink} app to be able to use Keycloak for authorization with OpenProject.', { htmlLink }, null, { escape: false, sanitize: false })
},
getConfigureOIDCHintText() {
const linkText = t('integration_openproject', 'User OIDC app')
const htmlLink = `<a class="link" href="" target="_blank" title="${linkText}">${linkText}</a>`
return t('integration_openproject', 'You can configure OIDC providers in the {htmlLink}.', { htmlLink }, null, { escape: false, sanitize: false })
},
isIntegrationCompleteWithOauth2() {
return (this.isServerHostFormComplete
&& this.isAuthorizationMethodFormComplete
&& this.isOPOAuthFormComplete
&& this.isNcOAuthFormComplete
&& this.isManagedGroupFolderSetUpComplete
&& !this.isOPUserAppPasswordFormInEdit
)
},
isIntegrationCompleteWithOIDC() {
return (this.isServerHostFormComplete
&& this.isAuthorizationMethodFormComplete
&& this.isAuthorizationSettingFormComplete
&& this.isManagedGroupFolderSetUpComplete
&& !this.isOPUserAppPasswordFormInEdit
)
},
isSetupCompleteWithoutProjectFolders() {
if (this.isProjectFolderSetupFormInEdit) {
return false
@ -590,6 +806,30 @@ export default {
return this.state.encryption_info.server_side_encryption_enabled
&& !this.state.encryption_info.encryption_enabled_for_groupfolders
},
getSelectedAuthenticatedMethod() {
return this.authorizationMethod.authorizationMethodSet === this.authMethods.OIDC
? this.authMethodsLabel.OIDC
: this.authMethodsLabel.OAUTH2
},
isAuthorizationMethodSelected() {
return this.authorizationMethod.currentAuthorizationMethodSelected === this.authorizationMethod.authorizationMethodSet
},
isAuthorizationSettingsSelected() {
const { oidcProviderSet, currentOIDCProviderSelected } = this.authorizationSetting
return currentOIDCProviderSelected === null
|| !this.getCurrentSelectedTargetedClientId
|| (oidcProviderSet === currentOIDCProviderSelected && this.authorizationSetting.targetedAudienceClientIdSet === this.getCurrentSelectedTargetedClientId)
|| (this.authorizationSetting.targetedAudienceClientIdSet === this.getCurrentSelectedTargetedClientId && oidcProviderSet === currentOIDCProviderSelected)
},
getCurrentSelectedOIDCProvider() {
return this.authorizationSetting.currentOIDCProviderSelected
},
getCurrentSelectedTargetedClientId() {
return this.state.authorization_settings.targeted_audience_client_id
},
isOIDCAppInstalledAndEnabled() {
return this.state.user_oidc_enabled
},
},
created() {
this.init()
@ -615,22 +855,70 @@ export default {
} else {
this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange
}
if (this.state.openproject_instance_url && this.state.openproject_client_id && this.state.openproject_client_secret && this.state.nc_oauth_client) {
// for oauth2 authorization
if (this.state.openproject_instance_url
&& this.state.openproject_client_id
&& this.state.openproject_client_secret
&& this.state.nc_oauth_client
) {
this.showDefaultManagedProjectFolders = true
}
// for oidc authorization
if (this.state.authorization_method === AUTH_METHOD.OIDC
&& this.state.openproject_instance_url
&& this.state.authorization_settings.oidc_provider
&& this.state.authorization_settings.targeted_audience_client_id
) {
this.showDefaultManagedProjectFolders = true
}
if (this.state.fresh_project_folder_setup === false) {
this.showDefaultManagedProjectFolders = true
}
if (this.state.openproject_instance_url) {
this.formMode.server = F_MODES.VIEW
this.isFormCompleted.server = true
}
if (this.state.authorization_method) {
this.formMode.authorizationMethod = F_MODES.VIEW
this.isFormCompleted.authorizationMethod = true
this.authorizationMethod.authorizationMethodSet = this.authorizationMethod.currentAuthorizationMethodSelected = this.state.authorization_method
}
if (this.state.openproject_instance_url && this.state.authorization_method) {
if (this.state.authorization_method === AUTH_METHOD.OAUTH2) {
if (!this.state.openproject_client_id || !this.state.openproject_client_secret) {
this.formMode.authorizationSetting = F_MODES.EDIT
}
}
if (this.state.authorization_method === AUTH_METHOD.OIDC) {
if (!this.state.authorization_settings.oidc_provider || !this.state.authorization_settings.targeted_audience_client_id) {
this.formMode.authorizationSetting = F_MODES.EDIT
}
}
}
if (this.state.authorization_method === AUTH_METHOD.OIDC
&& this.state.authorization_settings.oidc_provider
&& this.state.authorization_settings.targeted_audience_client_id
) {
this.formMode.authorizationSetting = F_MODES.VIEW
this.isFormCompleted.authorizationSetting = true
this.authorizationSetting.oidcProviderSet = this.authorizationSetting.currentOIDCProviderSelected = this.state.authorization_settings.oidc_provider
this.authorizationSetting.targetedAudienceClientIdSet = this.authorizationSetting.currentTargetedAudienceClientIdSelected = this.state.authorization_settings.targeted_audience_client_id
}
if (!!this.state.openproject_client_id && !!this.state.openproject_client_secret) {
this.formMode.opOauth = F_MODES.VIEW
this.isFormCompleted.opOauth = true
}
if (this.state.openproject_instance_url) {
if (!this.state.openproject_client_id || !this.state.openproject_client_secret) {
if (!this.state.authorization_method) {
this.formMode.authorizationMethod = F_MODES.EDIT
}
}
if (this.state.openproject_instance_url && this.state.authorization_method) {
if (!this.state.openproject_client_id && !this.state.openproject_client_secret) {
this.formMode.opOauth = F_MODES.EDIT
}
}
if (this.state.nc_oauth_client) {
this.formMode.ncOauth = F_MODES.VIEW
this.isFormCompleted.ncOauth = true
@ -644,7 +932,7 @@ export default {
this.formMode.projectFolderSetUp = F_MODES.VIEW
this.isFormCompleted.projectFolderSetUp = true
}
if (this.formMode.ncOauth === F_MODES.VIEW) {
if (this.formMode.ncOauth === F_MODES.VIEW || this.formMode.authorizationSetting === F_MODES.VIEW) {
this.showDefaultManagedProjectFolders = true
}
if (this.showDefaultManagedProjectFolders) {
@ -659,6 +947,10 @@ export default {
this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange
}
this.isProjectFolderSwitchEnabled = this.currentProjectFolderState === true
if (this.state.oidc_provider) {
this.registeredOidcProviders = this.state.oidc_provider
}
}
},
projectFolderSetUpErrorMessageDescription(errorKey) {
@ -678,12 +970,30 @@ export default {
setServerHostFormToViewMode() {
this.formMode.server = F_MODES.VIEW
},
setAuthorizationMethodToViewMode() {
this.formMode.authorizationMethod = F_MODES.VIEW
this.isFormCompleted.authorizationMethod = true
this.authorizationMethod.authorizationMethodSet = this.authorizationMethod.currentAuthorizationMethodSelected
},
setAuthorizationSettingToViewMode() {
this.formMode.authorizationSetting = F_MODES.VIEW
this.isFormCompleted.authorizationSetting = true
this.state.authorization_settings.targeted_audience_client_id = this.authorizationSetting.currentTargetedAudienceClientIdSelected
},
setServerHostFormToEditMode() {
this.formMode.server = F_MODES.EDIT
// set the edit variable to the current saved value
this.serverHostUrlForEdit = this.state.openproject_instance_url
this.isOpenProjectInstanceValid = null
},
setAuthorizationMethodInEditMode() {
this.formMode.authorizationMethod = F_MODES.EDIT
this.isFormCompleted.authorizationMethod = false
},
setAuthorizationSettingInEditMode() {
this.formMode.authorizationSetting = F_MODES.EDIT
this.isFormCompleted.authorizationSetting = false
},
setProjectFolderSetUpToEditMode() {
this.formMode.projectFolderSetUp = F_MODES.EDIT
this.isFormCompleted.projectFolderSetUp = false
@ -699,7 +1009,7 @@ export default {
async setNCOAuthFormToViewMode() {
this.formMode.ncOauth = F_MODES.VIEW
this.isFormCompleted.ncOauth = true
if (!this.isIntegrationComplete && this.formMode.projectFolderSetUp !== F_MODES.EDIT && this.formMode.opUserAppPassword !== F_MODES.EDIT) {
if (!this.isIntegrationCompleteWithOauth2 && this.formMode.projectFolderSetUp !== F_MODES.EDIT && this.formMode.opUserAppPassword !== F_MODES.EDIT) {
this.formMode.projectFolderSetUp = F_MODES.EDIT
this.showDefaultManagedProjectFolders = true
this.isProjectFolderSwitchEnabled = true
@ -767,8 +1077,8 @@ export default {
this.state.openproject_instance_url = this.serverHostUrlForEdit
this.formMode.server = F_MODES.VIEW
this.isFormCompleted.server = true
if (!this.isFormCompleted.opOauth) {
this.formMode.opOauth = F_MODES.EDIT
if (!this.isFormCompleted.authorizationMethod) {
this.formMode.authorizationMethod = F_MODES.EDIT
}
}
}
@ -787,6 +1097,42 @@ export default {
}
}
},
async saveAuthorizationMethodValue() {
this.isFormStep = FORM.AUTHORIZATION_METHOD
this.loadingAuthorizationMethodForm = true
const success = await this.saveOPOptions()
if (success) {
this.authorizationMethod.currentAuthorizationMethodSelected = this.authorizationMethod.authorizationMethodSet
this.formMode.authorizationMethod = F_MODES.VIEW
this.isFormCompleted.authorizationMethod = true
if (this.authorizationMethod.authorizationMethodSet === this.authMethods.OIDC && !this.isFormCompleted.authorizationSetting) {
this.formMode.authorizationSetting = F_MODES.EDIT
} else {
if (!this.isFormCompleted.opOauth) {
this.formMode.opOauth = F_MODES.EDIT
}
}
}
this.loadingAuthorizationMethodForm = false
},
async saveOIDCAuthSetting() {
this.isFormStep = FORM.AUTHORIZATION_SETTING
this.loadingAuthorizationMethodForm = true
this.authorizationSetting.oidcProviderSet = this.getCurrentSelectedOIDCProvider
this.authorizationSetting.targetedAudienceClientIdSet = this.state.authorization_settings.targeted_audience_client_id
const success = await this.saveOPOptions()
if (success) {
this.formMode.authorizationSetting = F_MODES.VIEW
this.isFormCompleted.authorizationSetting = true
if (!this.isIntegrationCompleteWithOIDC && this.formMode.projectFolderSetUp !== F_MODES.EDIT && this.formMode.opUserAppPassword !== F_MODES.EDIT) {
this.formMode.projectFolderSetUp = F_MODES.EDIT
this.showDefaultManagedProjectFolders = true
this.isProjectFolderSwitchEnabled = true
this.textLabelProjectFolderSetupButton = this.buttonTextLabel.completeWithProjectFolderSetup
}
}
this.loadingAuthorizationMethodForm = false
},
resetOPOAuthClientValues() {
OC.dialogs.confirmDestructive(
t('integration_openproject', 'If you proceed you will need to update these settings with the new OpenProject OAuth credentials. Also, all users will need to reauthorize access to their OpenProject account.'),
@ -805,6 +1151,39 @@ export default {
true,
)
},
async selectAuthorizationMethod() {
// open the confirmation dialog when only swithing back and forth between two authorization method
if (this.isAuthorizationFormInEditMode && this.authorizationMethod.currentAuthorizationMethodSelected !== null) {
await OC.dialogs.confirmDestructive(
t('integration_openproject', `If you proceed this method, you will have an ${this.authorizationMethod.authorizationMethodSet.toUpperCase()} based authorization configuration which will delete all the configuration setting for current ${this.authorizationMethod.currentAuthorizationMethodSelected.toUpperCase()} based authorization. You can switch back to it anytime.`),
t('integration_openproject', 'Switch Authorization Method'),
{
type: OC.dialogs.YES_NO_BUTTONS,
confirm: t('integration_openproject', 'Yes, switch'),
confirmClasses: 'error',
cancel: t('integration_openproject', 'Cancel'),
},
async (result) => {
if (result) {
// here we switch either to oidc or oauth2 configuration
const authMethod = this.authorizationMethod.authorizationMethodSet
if (authMethod === AUTH_METHOD.OAUTH2) {
this.state.authorization_settings.targeted_audience_client_id = null
this.authorizationSetting.currentOIDCProviderSelected = null
} else {
this.state.openproject_client_id = ''
this.state.openproject_client_secret = ''
}
await this.saveAuthorizationMethodValue()
}
window.location.reload()
},
true,
)
return ''
}
await this.saveAuthorizationMethodValue()
},
async clearOPOAuthClientValues() {
this.isFormStep = FORM.OP_OAUTH
this.formMode.opOauth = F_MODES.EDIT
@ -829,13 +1208,14 @@ export default {
},
async (result) => {
if (result) {
await this.resetAllAppValues()
const authMethod = this.authorizationMethod.authorizationMethodSet
await this.resetAllAppValues(authMethod)
}
},
true,
)
},
async resetAllAppValues() {
async resetAllAppValues(authMethod) {
// to avoid general console errors, we need to set the form to
// editor mode so that we can update the form fields with null values
// also, form completeness should be set to false
@ -843,12 +1223,18 @@ export default {
this.isFormCompleted.opOauth = false
this.formMode.server = F_MODES.EDIT
this.isFormCompleted.server = false
this.state.openproject_client_id = null
this.state.openproject_client_secret = null
this.state.default_enable_navigation = false
this.state.openproject_instance_url = null
this.state.default_enable_unified_search = false
this.oPUserAppPassword = null
this.authorizationMethod.authorizationMethodSet = null
this.state.openproject_client_id = null
this.state.openproject_client_secret = null
this.state.openproject_instance_url = null
// if the authorization method is "oidc"
if (authMethod === AUTH_METHOD.OIDC) {
this.state.authorization_settings.targeted_audience_client_id = null
this.authorizationSetting.currentOIDCProviderSelected = null
}
await this.saveOPOptions()
window.location.reload()
},
@ -952,21 +1338,46 @@ export default {
default_enable_navigation: this.state.default_enable_navigation,
default_enable_unified_search: this.state.default_enable_unified_search,
}
if (this.state.openproject_instance_url === null && this.state.openproject_client_secret === null && this.state.openproject_client_id === null) {
// doing whole reset
if (this.state.openproject_instance_url === null && this.authorizationMethod.authorizationMethodSet === null) {
// by default, it will be an oauth2 reset
values = {
...values,
authorization_method: this.authorizationMethod.authorizationMethodSet,
setup_project_folder: false,
setup_app_password: false,
}
if (this.authorizationMethod.currentAuthorizationMethodSelected === AUTH_METHOD.OIDC
&& this.authorizationSetting.currentOIDCProviderSelected === null
&& this.state.authorization_settings.targeted_audience_client_id === null) {
// when reset is oidc
values = {
...values,
oidc_provider: this.getCurrentSelectedOIDCProvider,
targeted_audience_client_id: this.getCurrentSelectedTargetedClientId,
}
}
} else if (this.isFormStep === FORM.AUTHORIZATION_SETTING) {
values = {
oidc_provider: this.getCurrentSelectedOIDCProvider,
targeted_audience_client_id: this.getCurrentSelectedTargetedClientId,
}
} else if (this.isFormStep === FORM.AUTHORIZATION_METHOD) {
values = {
...values,
authorization_method: this.authorizationMethod.authorizationMethodSet,
oidc_provider: this.isIntegrationCompleteWithOIDC ? this.getCurrentSelectedOIDCProvider : null,
targeted_audience_client_id: this.isIntegrationCompleteWithOIDC ? this.getCurrentSelectedTargetedClientId : null,
}
} else if (this.isFormStep === FORM.GROUP_FOLDER) {
if (!this.isProjectFolderSwitchEnabled) {
values = {
authorization_method: this.authorizationMethod.authorizationMethodSet,
setup_project_folder: false,
setup_app_password: false,
}
} else if (this.isProjectFolderSwitchEnabled === true) {
values = {
authorization_method: this.authorizationMethod.authorizationMethodSet,
setup_project_folder: !this.isProjectFolderAlreadySetup,
setup_app_password: this.opUserAppPassword !== true,
}
@ -1136,6 +1547,9 @@ export default {
)
})
},
onSelectOIDCProvider(selectedOption) {
this.authorizationSetting.currentOIDCProviderSelected = selectedOption
},
},
}
</script>
@ -1218,11 +1632,45 @@ export default {
}
.note-card {
max-width: 900px;
&--info-description, &--error-description, &--warning-description {
.link {
color: #1a67a3 !important;
font-style: normal;
}
.link {
color: #1a67a3 !important;
font-style: normal;
}
.authorization-method {
&--description {
font-size: 14px;
.title {
font-weight: 700;
}
.description {
margin-top: 0.1rem;
}
}
&--options {
margin-top: 1rem;
.radio-check {
font-weight: 500;
}
.oidc-app-check-description {
margin-left: 2.4rem;
font-size: 14px;
}
}
}
.authorization-settings {
&--content {
max-width: 550px;
&--label {
font-weight: 700;
font-size: .875rem;
}
&--client {
margin-top: 0.7rem;
}
}
.description {
margin-top: 0.1rem;
}
}
}

Просмотреть файл

@ -1,12 +1,15 @@
<template>
<div class="openproject-prefs section">
<SettingsTitle is-setting="personal" />
<div v-if="isNonOidcUserConnectedViaOidc" class="demo-error-oidc">
{{ t('integration_openproject', 'This feature is not available for this user account :)') }}
</div>
<div v-if="connected" class="openproject-prefs--connected">
<label>
<CheckIcon :size="20" />
{{ t('integration_openproject', 'Connected as {user}', { user: state.user_name }) }}
</label>
<NcButton class="openproject-prefs--disconnect" @click="disconnectFromOP()">
<NcButton v-if="state.authorization_method === authMethods.OAUTH2" class="openproject-prefs--disconnect" @click="disconnectFromOP()">
<template #icon>
<CloseIcon :size="23" />
</template>
@ -35,7 +38,7 @@
</template>
</CheckBox>
</div>
<OAuthConnectButton v-else :is-admin-config-ok="state.admin_config_ok" />
<OAuthConnectButton v-if="showOAuthConnectButton" :is-admin-config-ok="state.admin_config_ok" />
</div>
</template>
@ -51,7 +54,7 @@ import SettingsTitle from '../components/settings/SettingsTitle.vue'
import OAuthConnectButton from './OAuthConnectButton.vue'
import CheckBox from './settings/CheckBox.vue'
import { translate as t } from '@nextcloud/l10n'
import { checkOauthConnectionResult, USER_SETTINGS } from '../utils.js'
import { checkOauthConnectionResult, USER_SETTINGS, AUTH_METHOD } from '../utils.js'
import { NcButton } from '@nextcloud/vue'
export default {
@ -68,6 +71,7 @@ export default {
oauthConnectionErrorMessage: loadState('integration_openproject', 'oauth-connection-error-message'),
oauthConnectionResult: loadState('integration_openproject', 'oauth-connection-result'),
userSettingDescription: USER_SETTINGS,
authMethods: AUTH_METHOD,
}
},
computed: {
@ -76,6 +80,15 @@ export default {
return this.state.token && this.state.token !== ''
&& this.state.user_name && this.state.user_name !== ''
},
isNonOidcUserConnectedViaOidc() {
return !!(this.state.authorization_method === AUTH_METHOD.OIDC && this.state.admin_config_ok && !this.state.token)
},
showOAuthConnectButton() {
if (this.connected) {
return false
}
return !(this.state.admin_config_ok === true && this.state.authorization_method === this.authMethods.OIDC)
},
},
watch: {
'state.search_enabled'(newVal) {
@ -91,7 +104,9 @@ export default {
},
mounted() {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
if (this.state.authorization_method === this.authMethods.OAUTH2) {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
}
},
methods: {
@ -164,5 +179,9 @@ export default {
text-align: left;
padding: 0;
}
.demo-error-oidc {
color: red;
margin-top: 20px;
}
}
</style>

Просмотреть файл

@ -17,7 +17,9 @@
</div>
</div>
<div v-if="showConnectButton" class="empty-content--connect-button">
<OAuthConnectButton :is-admin-config-ok="isAdminConfigOk" :file-info="fileInfo" />
<OAuthConnectButton
:is-admin-config-ok="isAdminConfigOk"
:file-info="fileInfo" />
</div>
</div>
</div>
@ -31,7 +33,7 @@ import OpenProjectIcon from '../icons/OpenProjectIcon.vue'
import { generateUrl } from '@nextcloud/router'
import { translate as t } from '@nextcloud/l10n'
import OAuthConnectButton from '../OAuthConnectButton.vue'
import { STATE } from '../../utils.js'
import { AUTH_METHOD, STATE } from '../../utils.js'
export default {
name: 'EmptyContent',
@ -42,6 +44,10 @@ export default {
required: true,
default: STATE.OK,
},
authMethod: {
type: String,
required: true,
},
isAdminConfigOk: {
type: Boolean,
required: true,
@ -79,7 +85,11 @@ export default {
return this.state === STATE.OK
},
showConnectButton() {
return [STATE.NO_TOKEN, STATE.ERROR].includes(this.state)
// show button when admin config is not configured (either oidc or be oauth2)
if (!this.isAdminConfigOk) {
return true
}
return this.authMethod === AUTH_METHOD.OAUTH2 && [STATE.NO_TOKEN, STATE.ERROR].includes(this.state)
},
emptyContentTitleMessage() {
if (this.state === STATE.NO_TOKEN) {

Просмотреть файл

@ -43,10 +43,12 @@ export const F_MODES = {
export const FORM = {
SERVER: 0,
OP_OAUTH: 1,
NC_OAUTH: 2,
GROUP_FOLDER: 3,
APP_PASSWORD: 4,
AUTHORIZATION_METHOD: 1,
AUTHORIZATION_SETTING: 2,
OP_OAUTH: 3,
NC_OAUTH: 4,
GROUP_FOLDER: 5,
APP_PASSWORD: 6,
}
export const WORKPACKAGES_SEARCH_ORIGIN = {
@ -63,3 +65,13 @@ export const NO_OPTION_TEXT_STATE = {
SEARCHING: 1,
RESULT: 2,
}
export const AUTH_METHOD = {
OAUTH2: 'oauth2',
OIDC: 'oidc',
}
export const AUTH_METHOD_LABEL = {
OAUTH2: t('integration_openproject', 'OAuth2 two-way authorization code flow'),
OIDC: t('integration_openproject', 'OpenID identity provider'),
}

Просмотреть файл

@ -6,11 +6,17 @@
:loading="isLoading"
@markAsRead="onMarkAsRead">
<template #empty-content>
<EmptyContent v-if="emptyContentMessage"
id="openproject-empty-content"
:state="state"
:dashboard="true"
:is-admin-config-ok="isAdminConfigOk" />
<div v-if="isNonOidcUserConnectedViaOidc" class="demo-error-oidc">
{{ t('integration_openproject', 'This feature is not available for this user account :)') }}
</div>
<div v-else>
<EmptyContent v-if="emptyContentMessage"
id="openproject-empty-content"
:state="state"
:auth-method="authMethod"
:dashboard="true"
:is-admin-config-ok="isAdminConfigOk" />
</div>
</template>
</NcDashboardWidget>
</template>
@ -21,7 +27,7 @@ import { generateUrl } from '@nextcloud/router'
import { NcDashboardWidget } from '@nextcloud/vue'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { checkOauthConnectionResult, STATE } from '../utils.js'
import { AUTH_METHOD, checkOauthConnectionResult, STATE } from '../utils.js'
import { translate as t } from '@nextcloud/l10n'
import EmptyContent from '../components/tab/EmptyContent.vue'
@ -46,7 +52,9 @@ export default {
state: STATE.LOADING,
oauthConnectionErrorMessage: loadState('integration_openproject', 'oauth-connection-error-message'),
oauthConnectionResult: loadState('integration_openproject', 'oauth-connection-result'),
isAdminConfigOk: loadState('integration_openproject', 'admin-config-status'),
isAdminConfigOk: loadState('integration_openproject', 'admin_config_ok'),
userHasOidcToken: loadState('integration_openproject', 'user-has-oidc-token'),
authMethod: loadState('integration_openproject', 'authorization_method'),
settingsUrl: generateUrl('/settings/user/openproject'),
themingColor: OCA.Theming ? OCA.Theming.color.replace('#', '') : '0082C9',
windowVisibility: true,
@ -56,6 +64,7 @@ export default {
icon: 'icon-checkmark',
},
},
authMethods: AUTH_METHOD,
}
},
computed: {
@ -68,6 +77,9 @@ export default {
showMoreUrl() {
return this.openprojectUrl + '/notifications'
},
isNonOidcUserConnectedViaOidc() {
return !!(this.authMethod === AUTH_METHOD.OIDC && this.isAdminConfigOk && !this.userHasOidcToken)
},
items() {
const notifications = []
for (const key in this.notifications) {
@ -85,6 +97,10 @@ export default {
return notifications
},
emptyContentMessage() {
// for oidc connection currently we do not show any error to user
if (this.isNonOidcUserConnectedViaOidc === true) {
return
}
if (this.state === STATE.NO_TOKEN) {
return t('integration_openproject', 'No connection with OpenProject')
} else if (this.state === STATE.CONNECTION_ERROR) {
@ -108,7 +124,9 @@ export default {
},
},
mounted() {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
if (this.authMethod === this.authMethods.OAUTH2) {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
}
},
beforeDestroy() {
@ -279,4 +297,9 @@ export default {
:deep(.connect-button) {
margin-top: 10px;
}
.demo-error-oidc {
color: red;
margin-top: 20px;
}
</style>

Просмотреть файл

@ -57,6 +57,7 @@
<EmptyContent
id="openproject-empty-content"
:state="state"
:auth-method="authMethod"
:is-multiple-workpackage-linking="true"
:is-admin-config-ok="isAdminConfigOk" />
</div>
@ -105,7 +106,8 @@ export default {
state: STATE.LOADING,
fileInfos: [],
alreadyLinkedWorkPackage: [],
isAdminConfigOk: loadState('integration_openproject', 'admin-config-status'),
isAdminConfigOk: loadState('integration_openproject', 'admin_config_ok'),
authMethod: loadState('integration_openproject', 'authorization_method'),
oauthConnectionErrorMessage: loadState('integration_openproject', 'oauth-connection-error-message'),
oauthConnectionResult: loadState('integration_openproject', 'oauth-connection-result'),
searchOrigin: WORKPACKAGES_SEARCH_ORIGIN.LINK_MULTIPLE_FILES_MODAL,
@ -148,7 +150,9 @@ export default {
},
mounted() {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
if (this.authMethod === 'oauth2') {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
}
},
methods: {
async relinkRemainingFilesToWorkPackage() {

Просмотреть файл

@ -58,6 +58,7 @@
id="openproject-empty-content"
:state="state"
:file-info="fileInfo"
:auth-method="authMethod"
:is-admin-config-ok="isAdminConfigOk" />
</div>
</template>
@ -76,7 +77,7 @@ import { showSuccess, showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { loadState } from '@nextcloud/initial-state'
import { workpackageHelper } from '../utils/workpackageHelper.js'
import { STATE, WORKPACKAGES_SEARCH_ORIGIN, checkOauthConnectionResult } from '../utils.js'
import { STATE, WORKPACKAGES_SEARCH_ORIGIN, AUTH_METHOD, checkOauthConnectionResult } from '../utils.js'
export default {
name: 'ProjectsTab',
@ -96,7 +97,8 @@ export default {
workpackages: [],
oauthConnectionErrorMessage: loadState('integration_openproject', 'oauth-connection-error-message'),
oauthConnectionResult: loadState('integration_openproject', 'oauth-connection-result'),
isAdminConfigOk: loadState('integration_openproject', 'admin-config-status'),
isAdminConfigOk: loadState('integration_openproject', 'admin_config_ok'),
authMethod: loadState('integration_openproject', 'authorization_method'),
color: null,
openprojectUrl: loadState('integration_openproject', 'openproject-url'),
searchOrigin: WORKPACKAGES_SEARCH_ORIGIN.PROJECT_TAB,
@ -119,7 +121,9 @@ export default {
},
},
mounted() {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
if (this.authMethod === AUTH_METHOD.OAUTH2) {
checkOauthConnectionResult(this.oauthConnectionResult, this.oauthConnectionErrorMessage)
}
},
methods: {
/**

Просмотреть файл

@ -28,7 +28,7 @@
ref="linkPicker"
:is-smart-picker="true"
:file-info="fileInfo"
:is-disabled="isLoading || !isAdminConfigOk || !isStateOk"
:is-disabled="isLoading || !enableSearchInput || !isStateOk"
:linked-work-packages="linkedWorkPackages"
@submit="onSubmit" />
<div id="openproject-empty-content">
@ -36,9 +36,10 @@
<EmptyContent
v-else
:state="state"
:auth-method="adminConfigState.authMethod"
:file-info="fileInfo"
:is-smart-picker="true"
:is-admin-config-ok="isAdminConfigOk" />
:is-admin-config-ok="adminConfigState.isAdminConfigOk" />
</div>
</div>
</template>
@ -46,7 +47,7 @@
<script>
import SearchInput from '../components/tab/SearchInput.vue'
import EmptyContent from '../components/tab/EmptyContent.vue'
import { STATE } from '../utils.js'
import { AUTH_METHOD, STATE } from '../utils.js'
import { loadState } from '@nextcloud/initial-state'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
@ -76,7 +77,7 @@ export default {
fileInfo: {},
linkedWorkPackages: [],
state: STATE.LOADING,
isAdminConfigOk: loadState('integration_openproject', 'admin-config-status'),
adminConfigState: loadState('integration_openproject', 'admin-config'),
}
},
computed: {
@ -86,6 +87,15 @@ export default {
isLoading() {
return this.state === STATE.LOADING
},
enableSearchInput() {
if (this.adminConfigState.authMethod === AUTH_METHOD.OAUTH2 && this.adminConfigState.isAdminConfigOk) {
return true
}
if (this.adminConfigState.authMethod === AUTH_METHOD.OIDC && this.adminConfigState.isAdminConfigOk) {
return true
}
return false
},
},
mounted() {
this.checkIfOpenProjectIsAvailable()
@ -95,7 +105,7 @@ export default {
this.$emit('submit', data)
},
async checkIfOpenProjectIsAvailable() {
if (!this.isAdminConfigOk) {
if (!this.adminConfigState.isAdminConfigOk) {
this.state = STATE.ERROR
return
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -4,6 +4,7 @@ import { shallowMount, createLocalVue, mount } from '@vue/test-utils'
import PersonalSettings from '../../../src/components/PersonalSettings.vue'
import * as dialogs from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import { AUTH_METHOD } from '../../../src/utils.js'
const localVue = createLocalVue()
@ -90,7 +91,7 @@ describe('PersonalSettings.vue', () => {
describe('when username and token are given', () => {
beforeEach(async () => {
await wrapper.setData({
state: { user_name: 'test', token: '123', admin_config_ok: true },
state: { user_name: 'test', token: '123', admin_config_ok: true, authorization_method: AUTH_METHOD.OAUTH2 },
})
})
it('oAuth connect button is not displayed', () => {

Просмотреть файл

@ -1,5 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdminSettings.vue Authorization Method view mode form complete should show field values and hide the form completed with oauth2 auth method 1`] = `
Wrapper {
"selector": ".authorization-method",
}
`;
exports[`AdminSettings.vue Authorization Method view mode form complete should show field values and hide the form completed with oidc auth method 1`] = `
Wrapper {
"selector": ".authorization-method",
}
`;
exports[`AdminSettings.vue Authorization settings view mode form complete should show field values and hide authorization settings form 1`] = `
Wrapper {
"selector": ".authorization-settings",
}
`;
exports[`AdminSettings.vue Nextcloud OAuth values form edit mode should show the form and hide the field values 1`] = `
Wrapper {
"selector": ".nextcloud-oauth-values",
@ -31,7 +49,7 @@ Wrapper {
`;
exports[`AdminSettings.vue default user configurations form should be visible when the integration is complete 1`] = `
Wrapper {
ErrorWrapper {
"selector": ".default-prefs",
}
`;

Просмотреть файл

@ -1,7 +1,7 @@
/* jshint esversion: 8 */
import { shallowMount, createLocalVue } from '@vue/test-utils'
import EmptyContent from '../../../../src/components/tab/EmptyContent.vue'
import { STATE } from '../../../../src/utils.js'
import { AUTH_METHOD, STATE } from '../../../../src/utils.js'
const localVue = createLocalVue()
describe('EmptyContent.vue', () => {
@ -55,6 +55,7 @@ function getWrapper(propsData = {}) {
propsData: {
state: 'ok',
isAdminConfigOk: true,
authMethod: AUTH_METHOD.OAUTH2,
...propsData,
},
})

Просмотреть файл

@ -473,10 +473,11 @@ class ConfigControllerTest extends TestCase {
/**
* @return array<mixed>
*/
public function setAdminConfigStatusDataProvider() {
public function setAdminConfigStatusDataProviderForOauth2() {
return [
[
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => '$client_id',
'openproject_client_secret' => '$client_secret',
'openproject_instance_url' => 'http://openproject.com',
@ -485,10 +486,12 @@ class ConfigControllerTest extends TestCase {
],
[
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => '',
'openproject_client_secret' => '$client_secret',
'openproject_instance_url' => 'http://openproject.com',
], false
],
false
],
];
}
@ -498,16 +501,17 @@ class ConfigControllerTest extends TestCase {
* @param bool $adminConfigStatus
*
* @return void
* @dataProvider setAdminConfigStatusDataProvider
* @dataProvider setAdminConfigStatusDataProviderForOauth2
*/
public function testSetAdminConfigForDifferentAdminConfigStatus($credsToUpdate, $adminConfigStatus) {
public function testSetAdminConfigForDifferentAdminConfigStatusForOauth2($credsToUpdate, $adminConfigStatus) {
$userManager = \OC::$server->getUserManager();
$configMock = $this->getMockBuilder(IConfig::class)->getMock();
$configMock
->expects($this->exactly(3))
->expects($this->exactly(4))
->method('setAppValue')
->withConsecutive(
['integration_openproject', 'authorization_method', $credsToUpdate['authorization_method']],
['integration_openproject', 'openproject_client_id', $credsToUpdate['openproject_client_id']],
['integration_openproject', 'openproject_client_secret', $credsToUpdate['openproject_client_secret']],
['integration_openproject', 'openproject_instance_url', $credsToUpdate['openproject_instance_url']]
@ -516,20 +520,24 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'nc_oauth_client_id', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method'],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url']
)
->willReturnOnConsecutiveCalls(
'http://localhost:3000',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
'',
'',
'123',
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$credsToUpdate['openproject_client_id'],
$credsToUpdate['openproject_client_secret'],
$credsToUpdate['openproject_instance_url']
@ -566,6 +574,129 @@ class ConfigControllerTest extends TestCase {
);
}
/**
* @return array<mixed>
*/
public function setAdminConfigStatusDataProviderForOIDC() {
return [
[
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'test-oidc-provider',
'targeted_audience_client_id' => 'test-client',
'openproject_instance_url' => 'http://openproject.com'
],
true
],
[
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => '',
'targeted_audience_client_id' => 'test-client',
'openproject_instance_url' => 'http://openproject.com'
],
false
],
[
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => '',
'targeted_audience_client_id' => '',
'openproject_instance_url' => 'http://openproject.com'
],
false
],
[
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'test-oidc-provider',
'targeted_audience_client_id' => '',
'openproject_instance_url' => 'http://openproject.com'
],
false
],
];
}
/**
* @param array<string> $credsToUpdate
* @param bool $adminConfigStatus
*
* @return void
* @dataProvider setAdminConfigStatusDataProviderForOIDC
*/
public function testSetAdminConfigForDifferentAdminConfigStatusForOIDC($credsToUpdate, $adminConfigStatus) {
$userManager = \OC::$server->getUserManager();
$configMock = $this->getMockBuilder(IConfig::class)->getMock();
$configMock
->expects($this->exactly(4))
->method('setAppValue')
->withConsecutive(
['integration_openproject', 'authorization_method', $credsToUpdate['authorization_method']],
['integration_openproject', 'oidc_provider', $credsToUpdate['oidc_provider']],
['integration_openproject', 'targeted_audience_client_id', $credsToUpdate['targeted_audience_client_id']],
['integration_openproject', 'openproject_instance_url', $credsToUpdate['openproject_instance_url']]
);
$configMock
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'oidc_provider'],
['integration_openproject', 'targeted_audience_client_id'],
['integration_openproject', 'nc_oauth_client_id', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method'],
['integration_openproject', 'oidc_provider'],
['integration_openproject', 'targeted_audience_client_id'],
['integration_openproject', 'openproject_instance_url']
)
->willReturnOnConsecutiveCalls(
'http://localhost:3000',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
'',
'',
'123',
'',
OpenProjectAPIService::AUTH_METHOD_OIDC,
$credsToUpdate['oidc_provider'],
$credsToUpdate['targeted_audience_client_id'],
$credsToUpdate['openproject_instance_url']
);
$apiService = $this->getMockBuilder(OpenProjectAPIService::class)
->disableOriginalConstructor()
->getMock();
$configController = new ConfigController(
'integration_openproject',
$this->createMock(IRequest::class),
$configMock,
$this->createMock(IURLGenerator::class),
$userManager,
$this->l,
$apiService,
$this->createMock(LoggerInterface::class),
$this->createMock(OauthService::class),
$this->createMock(SettingsController::class),
$this->createMock(IGroupManager::class),
$this->createMock(ISecureRandom::class),
$this->createMock(ISubAdmin::class),
'test101'
);
$result = $configController->setAdminConfig($credsToUpdate);
$this->assertSame(
[
'status' => $adminConfigStatus,
'oPOAuthTokenRevokeStatus' => '',
"oPUserAppPassword" => null
],
$result->getData()
);
}
/**
* @return array<mixed>
@ -574,11 +705,13 @@ class ConfigControllerTest extends TestCase {
return [
[ // everything changes so delete user values and change the oAuth Client
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'old-openproject_client_id',
'openproject_client_secret' => 'old-openproject_client_secret',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://openproject.com',
@ -588,11 +721,13 @@ class ConfigControllerTest extends TestCase {
],
[ // only client id changes so delete user values but don't change the oAuth Client
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'old-openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://openproject.com',
@ -602,11 +737,13 @@ class ConfigControllerTest extends TestCase {
],
[ // only client secret changes so delete user values but don't change the oAuth Client
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'old-openproject_client_secret',
'openproject_instance_url' => 'http://openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://openproject.com',
@ -616,11 +753,13 @@ class ConfigControllerTest extends TestCase {
],
[ //only the openproject_instance_url changes so don't delete the user values but change the oAuth Client
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://openproject.com',
@ -630,11 +769,13 @@ class ConfigControllerTest extends TestCase {
],
[ //everything cleared
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => null,
'openproject_client_id' => null,
'openproject_client_secret' => null,
'openproject_instance_url' => null,
@ -644,11 +785,13 @@ class ConfigControllerTest extends TestCase {
],
[ //everything cleared with empty strings
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'openproject_client_id',
'openproject_client_secret' => 'openproject_client_secret',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => '',
'openproject_client_id' => '',
'openproject_client_secret' => '',
'openproject_instance_url' => '',
@ -684,20 +827,24 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'nc_oauth_client_id', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url']
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
$oldCreds['openproject_instance_url'],
$oldCreds['authorization_method'],
$oldCreds['openproject_client_id'],
$oldCreds['openproject_client_secret'],
'123',
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$credsToUpdate['openproject_client_id'],
$credsToUpdate['openproject_client_secret'],
$credsToUpdate['openproject_instance_url']
@ -724,18 +871,22 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url']
)
->willReturnOnConsecutiveCalls(
$oldCreds['openproject_instance_url'],
$oldCreds['authorization_method'],
$oldCreds['openproject_client_id'],
$oldCreds['openproject_client_secret'],
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$credsToUpdate['openproject_client_id'],
$credsToUpdate['openproject_client_secret'],
$credsToUpdate['openproject_instance_url']
@ -855,6 +1006,7 @@ class ConfigControllerTest extends TestCase {
return [
[
[
'authorization_method' => null,
'openproject_client_id' => null,
'openproject_client_secret' => null,
'openproject_instance_url' => null,
@ -866,6 +1018,7 @@ class ConfigControllerTest extends TestCase {
],
[
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'client_id_changed',
'openproject_client_secret' => 'client_secret_changed',
'openproject_instance_url' => 'http://localhost:3000',
@ -890,6 +1043,7 @@ class ConfigControllerTest extends TestCase {
*/
public function testSetAdminConfigForOPOAuthTokenRevoke($newConfig, $adminConfigStatus, $mode) {
$oldAdminConfig = [
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'some_old_client_id',
'openproject_client_secret' => 'some_old_client_secret',
'openproject_instance_url' => 'http://localhost:3000',
@ -917,24 +1071,28 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id', ''],
['integration_openproject', 'openproject_client_secret', ''],
['integration_openproject', 'nc_oauth_client_id', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], // for user
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], // for user
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], // for the last check
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
$oldAdminConfig['openproject_instance_url'],
$oldAdminConfig['authorization_method'],
$oldAdminConfig['openproject_client_id'],
$oldAdminConfig['openproject_client_secret'],
'',
'',
'',
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$newConfig['openproject_client_id'],
$newConfig['openproject_client_secret'],
$newConfig['openproject_instance_url'],
@ -944,22 +1102,26 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id', ''],
['integration_openproject', 'openproject_client_secret', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
$oldAdminConfig['openproject_instance_url'],
$oldAdminConfig['authorization_method'],
$oldAdminConfig['openproject_client_id'],
$oldAdminConfig['openproject_client_secret'],
'',
'',
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$newConfig['openproject_client_id'],
$newConfig['openproject_client_secret'],
$newConfig['openproject_instance_url'],
@ -968,6 +1130,7 @@ class ConfigControllerTest extends TestCase {
$configMock
->method('setAppValue')
->withConsecutive(
['integration_openproject', 'authorization_method', $newConfig['authorization_method']],
['integration_openproject', 'openproject_client_id', $newConfig['openproject_client_id']],
['integration_openproject', 'openproject_client_secret', $newConfig['openproject_client_secret']],
['integration_openproject', 'openproject_instance_url', $newConfig['openproject_instance_url']],
@ -1069,11 +1232,13 @@ class ConfigControllerTest extends TestCase {
*/
public function testOPOAuthTokenRevokeErrors($errorCode, $exception, $errMessage) {
$oldAdminConfig = [
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'some_old_client_id',
'openproject_client_secret' => 'some_old_client_secret',
'openproject_instance_url' => 'http://localhost:3000',
];
$newAdminConfig = [
'authorization_method' => '',
'openproject_client_id' => '',
'openproject_client_secret' => '',
'openproject_instance_url' => '',
@ -1095,20 +1260,24 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id', ''],
['integration_openproject', 'openproject_client_secret', ''],
['integration_openproject', 'nc_oauth_client_id', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], // for the last check
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
$oldAdminConfig['openproject_instance_url'],
$oldAdminConfig['authorization_method'],
$oldAdminConfig['openproject_client_id'],
$oldAdminConfig['openproject_client_secret'],
'',
$errorCode,
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$newAdminConfig['openproject_client_id'],
$newAdminConfig['openproject_client_secret'],
$newAdminConfig['openproject_instance_url'],
@ -1116,6 +1285,7 @@ class ConfigControllerTest extends TestCase {
$configMock
->method('setAppValue')
->withConsecutive(
['integration_openproject', 'authorization_method', $newAdminConfig['authorization_method']],
['integration_openproject', 'openproject_client_id', $newAdminConfig['openproject_client_id']],
['integration_openproject', 'openproject_client_secret', $newAdminConfig['openproject_client_secret']],
['integration_openproject', 'openproject_instance_url', $newAdminConfig['openproject_instance_url']],
@ -1199,6 +1369,7 @@ class ConfigControllerTest extends TestCase {
*/
public function testOPOAuthTokenRevokeDoesNotOccurIfNoOPOAuthClientHasChanged() {
$oldAdminConfig = [
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'some_old_client_id',
'openproject_client_secret' => 'some_old_client_secret',
'openproject_instance_url' => 'http://localhost:3000',
@ -1218,12 +1389,14 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id', ''],
['integration_openproject', 'openproject_client_secret', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', '']
)
->willReturnOnConsecutiveCalls(
$oldAdminConfig['openproject_instance_url'],
$oldAdminConfig['authorization_method'],
$oldAdminConfig['openproject_client_id'],
$oldAdminConfig['openproject_client_secret'],
''
@ -1285,18 +1458,18 @@ class ConfigControllerTest extends TestCase {
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'openproject_client_id', ''],
['integration_openproject', 'openproject_client_secret', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url']
)
->willReturnOnConsecutiveCalls(
'http://localhost:3000',
'some_cilent_id',
'some_cilent_secret',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
'some_cilent_id',
'some_cilent_secret',
'http://localhost:3000'
@ -1351,6 +1524,7 @@ class ConfigControllerTest extends TestCase {
);
$result = $configControllerMock->setAdminConfig([
"authorization_method" => OpenProjectAPIService::AUTH_METHOD_OAUTH,
"setup_project_folder" => true,
"setup_app_password" => true
]);
@ -1432,4 +1606,384 @@ class ConfigControllerTest extends TestCase {
$data = $result->getData();
$this->assertEquals("Database Error!", $data['error']);
}
/**
* @return array<mixed>
*/
public function setAdminConfigForOIDCAuthSettingProvider() {
return [
[ // set info if the authorization settings are changed
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old-oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'oidc_provider',
'targeted_audience_client_id' => 'targeted_audience_client_id',
'openproject_instance_url' => 'http://openproject.com',
],
false,
'change'
],
[ // set info even if only 'targeted_audience_client_id' authorization settings are changed
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old-oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old_oidc_provider',
'targeted_audience_client_id' => 'new_targeted_audience_client_id',
'openproject_instance_url' => 'http://openproject.com',
],
false,
'change'
],
[ // setinfo even if only 'oidc_provider' authorization settings are changed
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old-oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'new_oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://openproject.com',
]
],
[ // set if authorization settings are empty string
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old-oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => '',
'targeted_audience_client_id' => '',
'openproject_instance_url' => 'http://openproject.com',
]
],
[ // set if authorization settings are null
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old-oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => null,
'targeted_audience_client_id' => null,
'openproject_instance_url' => 'http://openproject.com',
]
],
];
}
/**
* @group ignoreWithPHP8.0
* @param array<string> $oldCreds
* @param array<string> $credsToUpdate
* @param bool $deleteUserValues
* @param bool|string $updateNCOAuthClient false => don't touch the client, 'change' => update it, 'delete' => remove it
* @return void
* @dataProvider setAdminConfigForOIDCAuthSettingProvider
*/
public function testSetAdminConfigOIDCAuthSetting(
$oldCreds, $credsToUpdate
) {
$userManager = $this->checkForUsersCountBeforeTest();
$configMock = $this->getMockBuilder(IConfig::class)->getMock();
$oauthServiceMock = $this->createMock(OauthService::class);
$oauthSettingsControllerMock = $this->getMockBuilder(SettingsController::class)
->disableOriginalConstructor()
->getMock();
$configMock
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'oidc_provider'],
['integration_openproject', 'targeted_audience_client_id'],
['integration_openproject', 'nc_oauth_client_id', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'oidc_provider'],
['integration_openproject', 'targeted_audience_client_id'],
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
$oldCreds['openproject_instance_url'],
$oldCreds['authorization_method'],
$oldCreds['oidc_provider'],
$oldCreds['targeted_audience_client_id'],
'123',
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$credsToUpdate['oidc_provider'],
$credsToUpdate['targeted_audience_client_id'],
$credsToUpdate['openproject_instance_url']
);
$apiService = $this->getMockBuilder(OpenProjectAPIService::class)
->disableOriginalConstructor()
->getMock();
$configController = new ConfigController(
'integration_openproject',
$this->createMock(IRequest::class),
$configMock,
$this->createMock(IURLGenerator::class),
$userManager,
$this->l,
$apiService,
$this->createMock(LoggerInterface::class),
$oauthServiceMock,
$oauthSettingsControllerMock,
$this->createMock(IGroupManager::class),
$this->createMock(ISecureRandom::class),
$this->createMock(ISubAdmin::class),
'test101'
);
$configController->setAdminConfig($credsToUpdate);
}
/**
* @return array<mixed>
*/
public function setAdminConfigForOAuth2AlreadyConfiguredDataProvider() {
return [
[ // when switching from oauth2 to oidc, userdata gets deleted along with the nc client information
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'old-openproject_client_id',
'openproject_client_secret' => 'old-openproject_client_secret',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'openproject_client_id' => '',
'openproject_client_secret' => '',
'openproject_instance_url' => 'http://old-openproject.com',
]
],
[ // when resetting with OAUTH2 already configured, userdata gets deleted along with the nc client information
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'openproject_client_id' => 'old-openproject_client_id',
'openproject_client_secret' => 'old-openproject_client_secret',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => '',
'openproject_client_id' => '',
'openproject_client_secret' => '',
'openproject_instance_url' => '',
]
]
];
}
/**
* @group ignoreWithPHP8.0
* @param array<string> $oldCreds
* @param array<string> $credsToUpdate
* @return void
* @dataProvider setAdminConfigForOAuth2AlreadyConfiguredDataProvider
*/
public function testSetAdminConfigForOAuth2AlreadyConfigured(
$oldCreds, $credsToUpdate
) {
$userManager = $this->checkForUsersCountBeforeTest();
$this->user1 = $userManager->createUser('test101', 'test101');
$configMock = $this->getMockBuilder(IConfig::class)->getMock();
$oauthServiceMock = $this->createMock(OauthService::class);
$oauthSettingsControllerMock = $this->getMockBuilder(SettingsController::class)
->disableOriginalConstructor()
->getMock();
$configMock
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'nc_oauth_client_id', ''],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
$oldCreds['openproject_instance_url'],
$oldCreds['authorization_method'],
$oldCreds['openproject_client_id'],
$oldCreds['openproject_client_secret'],
'123',
'123',
'',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$credsToUpdate['openproject_client_id'],
$credsToUpdate['openproject_client_secret'],
$credsToUpdate['openproject_instance_url']
);
$oauthSettingsControllerMock
->expects($this->once())
->method('deleteClient')
->with(123);
$configMock
->expects($this->exactly(10)) // 5 times for each user
->method('deleteUserValue')
->withConsecutive(
['admin', 'integration_openproject', 'token'],
['admin', 'integration_openproject', 'login'],
['admin', 'integration_openproject', 'user_id'],
['admin', 'integration_openproject', 'user_name'],
['admin', 'integration_openproject', 'refresh_token'],
[$this->user1->getUID(), 'integration_openproject', 'token'],
[$this->user1->getUID(), 'integration_openproject', 'login'],
[$this->user1->getUID(), 'integration_openproject', 'user_id'],
[$this->user1->getUID(), 'integration_openproject', 'user_name'],
[$this->user1->getUID(), 'integration_openproject', 'refresh_token'],
);
$apiService = $this->getMockBuilder(OpenProjectAPIService::class)
->disableOriginalConstructor()
->getMock();
$configController = new ConfigController(
'integration_openproject',
$this->createMock(IRequest::class),
$configMock,
$this->createMock(IURLGenerator::class),
$userManager,
$this->l,
$apiService,
$this->createMock(LoggerInterface::class),
$oauthServiceMock,
$oauthSettingsControllerMock,
$this->createMock(IGroupManager::class),
$this->createMock(ISecureRandom::class),
$this->createMock(ISubAdmin::class),
'test101'
);
$configController->setAdminConfig($credsToUpdate);
}
/**
* @return array<mixed>
*/
public function setAdminConfigForOIDCAlreadyConfigured() {
return [
[ // when switching from oidc to oauth2, just the user information get deleted
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old-oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH,
'oidc_provider' => '',
'targeted_audience_client_id' => '',
'openproject_instance_url' => 'http://old-openproject.com',
]
],
[ // when switching from oidc to oauth2, just the user information get deleted
[
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OIDC,
'oidc_provider' => 'old-oidc_provider',
'targeted_audience_client_id' => 'old-targeted_audience_client_id',
'openproject_instance_url' => 'http://old-openproject.com',
],
[
'authorization_method' => '',
'oidc_provider' => '',
'targeted_audience_client_id' => '',
'openproject_instance_url' => '',
]
],
];
}
/**
* @group ignoreWithPHP8.0
* @param array<string> $oldCreds
* @param array<string> $credsToUpdate
* @return void
* @dataProvider setAdminConfigForOIDCAlreadyConfigured
*/
public function testSetAdminConfigForOIDCAlreadyConfigured(
$oldCreds, $credsToUpdate
) {
$userManager = $this->checkForUsersCountBeforeTest();
$configMock = $this->getMockBuilder(IConfig::class)->getMock();
$oauthServiceMock = $this->createMock(OauthService::class);
$oauthSettingsControllerMock = $this->getMockBuilder(SettingsController::class)
->disableOriginalConstructor()
->getMock();
$configMock
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'openproject_instance_url', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'oidc_provider'],
['integration_openproject', 'targeted_audience_client_id'],
['integration_openproject', 'oPOAuthTokenRevokeStatus', ''],
['integration_openproject', 'authorization_method', ''],
['integration_openproject', 'oidc_provider'],
['integration_openproject', 'targeted_audience_client_id'],
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
$oldCreds['openproject_instance_url'],
$oldCreds['authorization_method'],
$oldCreds['oidc_provider'],
$oldCreds['targeted_audience_client_id'],
'',
$credsToUpdate['authorization_method'],
$credsToUpdate['oidc_provider'],
$credsToUpdate['targeted_audience_client_id'],
$credsToUpdate['openproject_instance_url']
);
$configMock
->expects($this->exactly(2))
->method('deleteUserValue')
->withConsecutive(
['test101', 'integration_openproject', 'user_id'],
['test101', 'integration_openproject', 'user_name']
);
$apiService = $this->getMockBuilder(OpenProjectAPIService::class)
->disableOriginalConstructor()
->getMock();
$configController = new ConfigController(
'integration_openproject',
$this->createMock(IRequest::class),
$configMock,
$this->createMock(IURLGenerator::class),
$userManager,
$this->l,
$apiService,
$this->createMock(LoggerInterface::class),
$oauthServiceMock,
$oauthSettingsControllerMock,
$this->createMock(IGroupManager::class),
$this->createMock(ISecureRandom::class),
$this->createMock(ISubAdmin::class),
'test101'
);
$configController->setAdminConfig($credsToUpdate);
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -10,6 +10,7 @@
namespace OCA\OpenProject\Settings;
use OCA\OpenProject\Service\OpenProjectAPIService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
@ -32,11 +33,17 @@ class PersonalTest extends TestCase {
*/
private $initialState;
/**
* @var MockObject | OpenProjectAPIService
*/
private $openProjectService;
protected function setUp(): void {
parent::setUp();
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
$this->initialState = $this->getMockBuilder(IInitialState::class)->getMock();
$this->setting = new Personal($this->config, $this->initialState, "testUser");
$this->openProjectService = $this->getMockBuilder(OpenProjectAPIService::class)->disableOriginalConstructor()->getMock();
$this->setting = new Personal($this->config, $this->initialState, $this->openProjectService, "testUser");
}
/**
@ -96,8 +103,10 @@ class PersonalTest extends TestCase {
$this->config
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'authorization_method'],
['integration_openproject', 'default_enable_unified_search'],
['integration_openproject', 'default_enable_navigation'],
['integration_openproject', 'authorization_method'],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url'],
@ -105,7 +114,9 @@ class PersonalTest extends TestCase {
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
OpenProjectAPIService::AUTH_METHOD_OAUTH,
'0', '0',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
$clientId,
$clientSecret,
$oauthInstanceUrl,
@ -125,6 +136,7 @@ class PersonalTest extends TestCase {
'search_enabled' => false,
'navigation_enabled' => false,
'admin_config_ok' => $adminConfigStatus,
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH
]
],
['oauth-connection-result'],
@ -156,8 +168,10 @@ class PersonalTest extends TestCase {
$this->config
->method('getAppValue')
->withConsecutive(
['integration_openproject', 'authorization_method'],
['integration_openproject', 'default_enable_unified_search'],
['integration_openproject', 'default_enable_navigation'],
['integration_openproject', 'authorization_method'],
['integration_openproject', 'openproject_client_id'],
['integration_openproject', 'openproject_client_secret'],
['integration_openproject', 'openproject_instance_url'],
@ -165,7 +179,9 @@ class PersonalTest extends TestCase {
['integration_openproject', 'openproject_instance_url'],
)
->willReturnOnConsecutiveCalls(
OpenProjectAPIService::AUTH_METHOD_OAUTH,
'1', '1',
OpenProjectAPIService::AUTH_METHOD_OAUTH,
"some-client-id",
"some-client-secret",
"http://localhost",
@ -182,6 +198,7 @@ class PersonalTest extends TestCase {
'search_enabled' => true,
'navigation_enabled' => true,
'admin_config_ok' => true,
'authorization_method' => OpenProjectAPIService::AUTH_METHOD_OAUTH
]
],
['oauth-connection-result'],