Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WP #60683]display error if oidc apps not supported #768

Merged
merged 5 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Show errors when user_oidc app is not available [#753](https://github.com/nextcloud/integration_openproject/pull/753)
- Show proper error message in the dashboard based on auth method [#770](https://github.com/nextcloud/integration_openproject/pull/770)
- Drop application's support for Nextcloud 27 [#779](https://github.com/nextcloud/integration_openproject/pull/779)
- Show error when the user_oidc app not supported [#768](https://github.com/nextcloud/integration_openproject/pull/768)

### Fixed
- choose correct base URL for OCS requests [#780](https://github.com/nextcloud/integration_openproject/pull/780)
Expand Down
12 changes: 9 additions & 3 deletions lib/Service/OpenProjectAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
class OpenProjectAPIService {
public const AUTH_METHOD_OAUTH = 'oauth2';
public const AUTH_METHOD_OIDC = 'oidc';
public const MIN_SUPPORTED_USER_OIDC_APP_VERSION = '6.2.0';
/**
* @var string
*/
Expand Down Expand Up @@ -1676,13 +1677,18 @@ public function getRegisteredOidcProviders(): array {
}

public function isUserOIDCAppInstalledAndEnabled(): bool {
return $this->appManager->isInstalled('user_oidc');
}

public function isUserOIDCAppSupported(): bool {
$userOidcVersion = $this->appManager->getAppVersion('user_oidc');
return (
$this->isUserOIDCAppInstalledAndEnabled() &&
class_exists('\OCA\UserOIDC\Db\ProviderMapper') &&
class_exists('\OCA\UserOIDC\Event\ExchangedTokenRequestedEvent') &&
class_exists('\OCA\UserOIDC\Exception\TokenExchangeFailedException') &&
$this->appManager->isInstalled(
'user_oidc',
)
class_exists('\OCA\UserOIDC\User\Backend') &&
version_compare($userOidcVersion, self::MIN_SUPPORTED_USER_OIDC_APP_VERSION) >= 0
);
}

Expand Down
4 changes: 3 additions & 1 deletion lib/Settings/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ public function getForm(): TemplateResponse {
'encryption_enabled_for_groupfolders' => $this->config->getAppValue('groupfolders', 'enable_encryption', '') === 'true'
],
'oidc_provider' => $this->openProjectAPIService->getRegisteredOidcProviders(),
'user_oidc_enabled' => $this->openProjectAPIService->isUserOIDCAppInstalledAndEnabled()
'user_oidc_enabled' => $this->openProjectAPIService->isUserOIDCAppInstalledAndEnabled(),
'user_oidc_supported' => $this->openProjectAPIService->isUserOIDCAppSupported(),
'user_oidc_minimum_version' => OpenProjectAPIService::MIN_SUPPORTED_USER_OIDC_APP_VERSION,
];

$this->initialStateService->provideInitialState('admin-settings-config', $adminConfig);
Expand Down
28 changes: 23 additions & 5 deletions src/components/AdminSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,15 @@
<NcCheckboxRadioSwitch class="radio-check"
:checked.sync="authorizationMethod.authorizationMethodSet"
:value="authMethods.OIDC"
:disabled="!isOIDCAppInstalledAndEnabled"
:disabled="!isOIDCAppInstalledAndEnabled || !isOIDCAppSupported"
type="radio">
{{ authMethodsLabel.OIDC }}
</NcCheckboxRadioSwitch>
<p v-if="!isOIDCAppInstalledAndEnabled" class="oidc-app-check-description" v-html="getOIDCAppNotInstalledHintText" /> <!-- eslint-disable-line vue/no-v-html -->
<ErrorLabel
v-if="isOIDCAppInstalledAndEnabled && !isOIDCAppSupported"
:error="`${messagesFmt.appNotSupported('user_oidc')}. ${messagesFmt.minimumVersionRequired(getUserOidcMinimumVersion)}`"
type="error" />
</div>
</div>
<div v-else>
Expand Down Expand Up @@ -133,13 +137,19 @@
:is-complete="isAuthorizationSettingFormComplete"
:is-disabled="isAuthorizationSettingFormInDisabledMode"
:is-dark-theme="isDarkTheme"
:has-error="!isOIDCAppInstalledAndEnabled" />
:has-error="!isOIDCAppInstalledAndEnabled || !isOIDCAppSupported" />
<ErrorNote
v-if="!isOIDCAppInstalledAndEnabled"
:error-title="messagesFmt.appNotInstalled('user_oidc')"
:error-message="messages.appRequiredForOIDCMethod"
:error-link="appLinks.user_oidc.installLink"
:error-link-label="messages.downloadAndEnableApp" />
<ErrorNote
v-if="isOIDCAppInstalledAndEnabled && !isOIDCAppSupported"
:error-title="messagesFmt.appNotSupported('user_oidc')"
:error-message="messagesFmt.minimumVersionRequired(getUserOidcMinimumVersion)"
:error-link="appLinks.user_oidc.installLink"
:error-link-label="messages.downloadAndEnableApp" />
<div class="authorization-settings--content">
<FieldValue v-if="isAuthorizationSettingsInViewMode"
is-required
Expand All @@ -153,7 +163,7 @@
<div id="select">
<NcSelect
input-id="provider-search-input"
:disabled="!isOIDCAppInstalledAndEnabled"
:disabled="!isOIDCAppInstalledAndEnabled || !isOIDCAppSupported"
:placeholder="t('integration_openproject', 'Select an OIDC provider')"
:options="registeredOidcProviders"
:value="getCurrentSelectedOIDCProvider"
Expand All @@ -178,15 +188,15 @@
v-model="state.authorization_settings.targeted_audience_client_id"
class="py-1"
is-required
:disabled="!isOIDCAppInstalledAndEnabled"
:disabled="!isOIDCAppInstalledAndEnabled || !isOIDCAppSupported"
:place-holder="messages.opClientId"
:label="messages.opClientId"
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"
:disabled="!isOIDCAppInstalledAndEnabled"
:disabled="!isOIDCAppInstalledAndEnabled || !isOIDCAppSupported"
data-test-id="reset-auth-settings-btn"
@click="setAuthorizationSettingInEditMode">
<template #icon>
Expand Down Expand Up @@ -548,10 +558,12 @@ import TermsOfServiceUnsigned from './admin/TermsOfServiceUnsigned.vue'
import dompurify from 'dompurify'
import { messages, messagesFmt } from '../constants/messages.js'
import { appLinks } from '../constants/links.js'
import ErrorLabel from './ErrorLabel.vue'

export default {
name: 'AdminSettings',
components: {
ErrorLabel,
NcSelect,
NcButton,
FieldValue,
Expand Down Expand Up @@ -794,6 +806,9 @@ export default {
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 })
},
getUserOidcMinimumVersion() {
return this.state.user_oidc_minimum_version
},
isIntegrationCompleteWithOauth2() {
return (this.isServerHostFormComplete
&& this.isAuthorizationMethodFormComplete
Expand Down Expand Up @@ -854,6 +869,9 @@ export default {
isOIDCAppInstalledAndEnabled() {
return this.state.user_oidc_enabled
},
isOIDCAppSupported() {
return this.state.user_oidc_supported
},
},
created() {
this.init()
Expand Down
2 changes: 2 additions & 0 deletions src/constants/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export const messages = {

export const messagesFmt = {
appNotInstalled: (app) => t(APP_ID, 'The "{app}" app is not installed', { app }),
appNotSupported: (app) => t(APP_ID, 'The "{app}" app is not supported', { app }),
minimumVersionRequired: (minimumAppVersion) => t(APP_ID, 'Requires app version "{minimumAppVersion}" or later', { minimumAppVersion }),
}
91 changes: 80 additions & 11 deletions tests/jest/components/AdminSettings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ describe('AdminSettings.vue', () => {
axios.put.mockReset()
jest.clearAllMocks()
})
it('should disable "OpenID identity provider" radio button for user_oidc app not installed', async () => {
it('should disable "OpenID identity provider" radio button when an user_oidc app is not installed', async () => {
await wrapper.setData({
state: {
user_oidc_enabled: false,
Expand All @@ -890,10 +890,21 @@ describe('AdminSettings.vue', () => {
expect(openIDProviderDisabled.isVisible()).toBe(true)
})

it('should not disable "OpenID identity provider" radio button for user_oidc app installed', async () => {
it('should disable "OpenID identity provider" radio button when an unsupported user_oidc app is installed', async () => {
await wrapper.setData({
state: {
user_oidc_enabled: true,
user_oidc_supported: false,
},
})
expect(wrapper.find(selectors.openIdIdentityDisabled).exists()).toBe(true)
})

it('should not disable "OpenID identity provider" radio button for supported user_oidc app installed', async () => {
await wrapper.setData({
state: {
user_oidc_enabled: true,
user_oidc_supported: true,
},
})
expect(wrapper.find(selectors.openIdIdentityDisabled).exists()).toBe(false)
Expand All @@ -915,10 +926,11 @@ describe('AdminSettings.vue', () => {
expect(wrapper.vm.formMode.opOauth).toBe(F_MODES.EDIT)
})

it('should show authorization settings form for "oidc" method selected', async () => {
it('should show authorization settings form for "oidc" method selected for the supported OIDC app', async () => {
await wrapper.setData({
state: {
user_oidc_enabled: true,
user_oidc_supported: true,
authorization_settings: {
oidc_provider: null,
targeted_audience_client_id: null,
Expand Down Expand Up @@ -984,10 +996,11 @@ describe('AdminSettings.vue', () => {
expect(wrapper.vm.formMode.authorizationMethod).toBe(F_MODES.VIEW)
})

it('should enable "save" button when OIDC auth is selected', async () => {
it('should enable "save" button when OIDC auth is selected and the supported OIDC app is installed ', async () => {
await wrapper.setData({
state: {
user_oidc_enabled: true,
user_oidc_supported: true,
},
})
const authMethodSaveButton = authorizationMethodForm.find(selectors.authorizationMethodSaveButton)
Expand Down Expand Up @@ -1163,9 +1176,9 @@ describe('AdminSettings.vue', () => {
},
}

describe('user_oidc app enabled', () => {
describe('supported user_oidc app enabled', () => {
beforeEach(async () => {
wrapper = getWrapper({ state: { ...state, ...authorizationSettingsState, user_oidc_enabled: true } })
wrapper = getWrapper({ state: { ...state, ...authorizationSettingsState, user_oidc_enabled: true, user_oidc_supported: true } })
})
it('should show configured OIDC authorization', () => {
const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings)
Expand All @@ -1183,9 +1196,35 @@ describe('AdminSettings.vue', () => {
})
})

describe('user_oidc app disabled', () => {
describe('unsupported user_oidc app enabled', () => {
beforeEach(async () => {
wrapper = getWrapper({ state: { ...state, ...authorizationSettingsState, user_oidc_enabled: true, user_oidc_supported: false } })
})
it('should show field values and hide authorization settings form', () => {
const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings)
expect(wrapper.vm.isIntegrationCompleteWithOIDC).toBe(true)
expect(authorizationSettingsForm.element).toMatchSnapshot()
})
it('should disable reset button', () => {
const resetButton = wrapper.find(selectors.authorizationSettingsResetButton)
expect(resetButton.attributes().disabled).toBe('true')
})
it('should show app not supported error messages', () => {
const formHeader = wrapper.find(formHeaderSelector)
const errorNote = wrapper.find(errorNoteSelector)

expect(formHeader.exists()).toBe(true)
expect(formHeader.attributes().haserror).toBe('true')
expect(errorNote.exists()).toBe(true)
expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotSupported())
expect(errorNote.attributes().errormessage).toBe(messagesFmt.minimumVersionRequired())
expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink)
})
})

describe('supported user_oidc app disabled', () => {
beforeEach(async () => {
wrapper = getWrapper({ state: { ...state, ...authorizationSettingsState, user_oidc_enabled: false } })
wrapper = getWrapper({ state: { ...state, ...authorizationSettingsState, user_oidc_enabled: false, user_oidc_supported: true } })
})
it('should show field values and hide authorization settings form', () => {
const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings)
Expand All @@ -1210,7 +1249,7 @@ describe('AdminSettings.vue', () => {
})
})

describe('edit mode form, complete admin configuration', () => {
describe('edit mode form, complete admin configuration with supported user_oidc app', () => {
let wrapper, authorizationSettingsForm, authSettingsResetButton
beforeEach(async () => {
axios.put.mockReset()
Expand All @@ -1224,6 +1263,7 @@ describe('AdminSettings.vue', () => {
targeted_audience_client_id: 'some-target-aud-client-id',
},
user_oidc_enabled: true,
user_oidc_supported: true,
},
})
authorizationSettingsForm = wrapper.find(selectors.authorizationSettings)
Expand Down Expand Up @@ -1279,9 +1319,9 @@ describe('AdminSettings.vue', () => {
describe('edit mode, incomplete admin configuration', () => {
let wrapper

describe('user_oidc app enabled', () => {
describe('Supported user_oidc app enabled', () => {
beforeEach(async () => {
wrapper = getWrapper({ state: { ...state, user_oidc_enabled: true } })
wrapper = getWrapper({ state: { ...state, user_oidc_enabled: true, user_oidc_supported: true } })
})

it('should show authorization settings in edit mode without errors', () => {
Expand Down Expand Up @@ -1342,6 +1382,7 @@ describe('AdminSettings.vue', () => {
state: {
...state,
user_oidc_enabled: true,
user_oidc_supported: true,
},
})
const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings)
Expand Down Expand Up @@ -1406,6 +1447,34 @@ describe('AdminSettings.vue', () => {
expect(authClientInput.attributes().disabled).toBe('true')
})
})

describe('unsupported user_oidc app is enable', () => {
beforeEach(async () => {
wrapper = getWrapper({ state: { ...state, user_oidc_enabled: true, user_oidc_supported: false } })
})

it('should show app not supported error messages', () => {
const formHeaderError = wrapper.find(formHeaderSelector)
const errorNote = wrapper.find(errorNoteSelector)

expect(formHeaderError.exists()).toBe(true)
expect(formHeaderError.attributes().haserror).toBe('true')
expect(errorNote.exists()).toBe(true)
expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotSupported())
expect(errorNote.attributes().errormessage).toBe(messagesFmt.minimumVersionRequired())
expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink)
})
it('should disable form elements', () => {
const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings)
const authSettingsSaveButton = authorizationSettingsForm.find(selectors.authorizationSettingsSaveButton)
const authProviderSelect = wrapper.find(authProviderSelector)
const authClientInput = wrapper.find(authClientSelector)

expect(authSettingsSaveButton.attributes().disabled).toBe('true')
expect(authProviderSelect.attributes().disabled).toBe('true')
expect(authClientInput.attributes().disabled).toBe('true')
})
})
})
})

Expand Down
Loading
Loading