From 87cb2256682ae47e2dd09f28238205611c17cd0e Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Tue, 30 Sep 2025 18:19:26 +0200 Subject: [PATCH] refactor(user_ldap): Rewrite setup wizard Signed-off-by: Louis Chemineau --- .eslintrc.js | 2 + apps/files/src/views/Settings.vue | 1 - apps/user_ldap/appinfo/routes.php | 6 - apps/user_ldap/js/wizard/configModel.js | 4 + .../lib/Controller/ConfigAPIController.php | 9 +- apps/user_ldap/lib/Settings/Admin.php | 43 ++- apps/user_ldap/openapi.json | 59 ++-- apps/user_ldap/src/LDAPSettingsApp.vue | 11 + .../components/SettingsTabs/AdvancedTab.vue | 294 ++++++++++++++++++ .../src/components/SettingsTabs/ExpertTab.vue | 64 ++++ .../src/components/SettingsTabs/GroupsTab.vue | 158 ++++++++++ .../src/components/SettingsTabs/LoginTab.vue | 171 ++++++++++ .../src/components/SettingsTabs/ServerTab.vue | 192 ++++++++++++ .../src/components/SettingsTabs/UsersTab.vue | 180 +++++++++++ .../src/components/WizardControls.vue | 94 ++++++ apps/user_ldap/src/main.ts | 21 ++ apps/user_ldap/src/models/index.ts | 71 +++++ .../src/services/ldapConfigService.ts | 189 +++++++++++ apps/user_ldap/src/services/logger.ts | 11 + apps/user_ldap/src/store/configs.ts | 75 +++++ apps/user_ldap/src/store/index.ts | 8 + apps/user_ldap/src/views/Settings.vue | 186 +++++++++++ apps/user_ldap/templates/settings.php | 5 +- apps/user_ldap/tests/Settings/AdminTest.php | 8 +- openapi.json | 59 ++-- webpack.modules.js | 3 + 26 files changed, 1834 insertions(+), 90 deletions(-) create mode 100644 apps/user_ldap/src/LDAPSettingsApp.vue create mode 100644 apps/user_ldap/src/components/SettingsTabs/AdvancedTab.vue create mode 100644 apps/user_ldap/src/components/SettingsTabs/ExpertTab.vue create mode 100644 apps/user_ldap/src/components/SettingsTabs/GroupsTab.vue create mode 100644 apps/user_ldap/src/components/SettingsTabs/LoginTab.vue create mode 100644 apps/user_ldap/src/components/SettingsTabs/ServerTab.vue create mode 100644 apps/user_ldap/src/components/SettingsTabs/UsersTab.vue create mode 100644 apps/user_ldap/src/components/WizardControls.vue create mode 100644 apps/user_ldap/src/main.ts create mode 100644 apps/user_ldap/src/models/index.ts create mode 100644 apps/user_ldap/src/services/ldapConfigService.ts create mode 100644 apps/user_ldap/src/services/logger.ts create mode 100644 apps/user_ldap/src/store/configs.ts create mode 100644 apps/user_ldap/src/store/index.ts create mode 100644 apps/user_ldap/src/views/Settings.vue diff --git a/.eslintrc.js b/.eslintrc.js index f41e338fe9d..0ec03ea03ed 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,6 +32,8 @@ module.exports = { ignores: ['/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'], }], 'vue/html-self-closing': 'error', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param-description': 'off', }, settings: { jsdoc: { diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue index bfac8e0b3d6..0160e387892 100644 --- a/apps/files/src/views/Settings.vue +++ b/apps/files/src/views/Settings.vue @@ -127,7 +127,6 @@ -

{{ t('files', 'Actions') }}

diff --git a/apps/user_ldap/appinfo/routes.php b/apps/user_ldap/appinfo/routes.php index 52138060f58..688d8014555 100644 --- a/apps/user_ldap/appinfo/routes.php +++ b/apps/user_ldap/appinfo/routes.php @@ -23,12 +23,6 @@ $this->create('user_ldap_ajax_wizard', 'apps/user_ldap/ajax/wizard.php') ->actionInclude('user_ldap/ajax/wizard.php'); return [ - 'ocs' => [ - ['name' => 'ConfigAPI#create', 'url' => '/api/v1/config', 'verb' => 'POST'], - ['name' => 'ConfigAPI#show', 'url' => '/api/v1/config/{configID}', 'verb' => 'GET'], - ['name' => 'ConfigAPI#modify', 'url' => '/api/v1/config/{configID}', 'verb' => 'PUT'], - ['name' => 'ConfigAPI#delete', 'url' => '/api/v1/config/{configID}', 'verb' => 'DELETE'], - ], 'routes' => [ ['name' => 'renewPassword#tryRenewPassword', 'url' => '/renewpassword', 'verb' => 'POST'], ['name' => 'renewPassword#showRenewPasswordForm', 'url' => '/renewpassword/{user}', 'verb' => 'GET'], diff --git a/apps/user_ldap/js/wizard/configModel.js b/apps/user_ldap/js/wizard/configModel.js index 85c87e2ef15..b559fc33861 100644 --- a/apps/user_ldap/js/wizard/configModel.js +++ b/apps/user_ldap/js/wizard/configModel.js @@ -117,6 +117,8 @@ OCA = OCA || {}; * @returns {jqXHR} */ callWizard: function(params, callback, detector) { + console.debug('[LDAP - Legacy] Called wizard action', { params }) + return this.callAjax('wizard.php', params, callback, detector); }, @@ -180,6 +182,7 @@ OCA = OCA || {}; var strParams = OC.buildQueryString(objParams); var model = this; $.post(url, strParams, function(result) { model._processSetResult(model, result, objParams) }); + console.debug('[LDAP - Legacy] Saved value', { objParams }) return true; }, @@ -321,6 +324,7 @@ OCA = OCA || {}; var params = OC.buildQueryString({ldap_serverconfig_chooser: this.configID}); var model = this; $.post(url, params, function(result) { model._processTestResult(model, result) }); + console.debug('[LDAP - Legacy] Tested configuration', { params }) //TODO: make sure only one test is running at a time }, diff --git a/apps/user_ldap/lib/Controller/ConfigAPIController.php b/apps/user_ldap/lib/Controller/ConfigAPIController.php index d98e6d41b52..f417718fa46 100644 --- a/apps/user_ldap/lib/Controller/ConfigAPIController.php +++ b/apps/user_ldap/lib/Controller/ConfigAPIController.php @@ -14,6 +14,7 @@ use OCA\User_LDAP\ConnectionFactory; use OCA\User_LDAP\Helper; use OCA\User_LDAP\Settings\Admin; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; @@ -58,6 +59,7 @@ class ConfigAPIController extends OCSController { * 200: Config created successfully */ #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'POST', url: '/api/v1/config')] public function create() { try { $configPrefix = $this->ldapHelper->getNextServerConfigurationPrefix(); @@ -82,6 +84,7 @@ class ConfigAPIController extends OCSController { * 200: Config deleted successfully */ #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'DELETE', url: '/api/v1/config/{configID}')] public function delete($configID) { try { $this->ensureConfigIDExists($configID); @@ -103,7 +106,7 @@ class ConfigAPIController extends OCSController { * * @param string $configID ID of the config * @param array $configData New config - * @return DataResponse, array{}> + * @return DataResponse, array{}> * @throws OCSException * @throws OCSBadRequestException Modifying config is not possible * @throws OCSNotFoundException Config not found @@ -111,6 +114,7 @@ class ConfigAPIController extends OCSController { * 200: Config returned */ #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'PUT', url: '/api/v1/config/{configID}')] public function modify($configID, $configData) { try { $this->ensureConfigIDExists($configID); @@ -137,7 +141,7 @@ class ConfigAPIController extends OCSController { throw new OCSException('An issue occurred when modifying the config.'); } - return new DataResponse(); + return $this->show($configID, false); } /** @@ -215,6 +219,7 @@ class ConfigAPIController extends OCSController { * 200: Config returned */ #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'GET', url: '/api/v1/config/{configID}')] public function show($configID, $showPassword = false) { try { $this->ensureConfigIDExists($configID); diff --git a/apps/user_ldap/lib/Settings/Admin.php b/apps/user_ldap/lib/Settings/Admin.php index 89fb063265b..a3144fd9c57 100644 --- a/apps/user_ldap/lib/Settings/Admin.php +++ b/apps/user_ldap/lib/Settings/Admin.php @@ -9,6 +9,7 @@ namespace OCA\User_LDAP\Settings; use OCA\User_LDAP\Configuration; use OCA\User_LDAP\Helper; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\IL10N; use OCP\Server; use OCP\Settings\IDelegatedSettings; @@ -18,13 +19,11 @@ class Admin implements IDelegatedSettings { public function __construct( private IL10N $l, private ITemplateManager $templateManager, + private IInitialState $initialState, ) { } - /** - * @return TemplateResponse - */ - public function getForm() { + public function getForm(): TemplateResponse { $helper = Server::get(Helper::class); $prefixes = $helper->getServerConfigurationPrefixes(); if (count($prefixes) === 0) { @@ -35,19 +34,6 @@ class Admin implements IDelegatedSettings { $prefixes[] = $newPrefix; } - $hosts = $helper->getServerConfigurationHosts(); - - $wControls = $this->templateManager->getTemplate('user_ldap', 'part.wizardcontrols'); - $wControls = $wControls->fetchPage(); - $sControls = $this->templateManager->getTemplate('user_ldap', 'part.settingcontrols'); - $sControls = $sControls->fetchPage(); - - $parameters = []; - $parameters['serverConfigurationPrefixes'] = $prefixes; - $parameters['serverConfigurationHosts'] = $hosts; - $parameters['settingControls'] = $sControls; - $parameters['wizardControls'] = $wControls; - // assign default values if (!isset($config)) { $config = new Configuration('', false); @@ -57,13 +43,26 @@ class Admin implements IDelegatedSettings { $parameters[$key . '_default'] = $default; } + $ldapConfigs = []; + foreach ($prefixes as $prefix) { + $ldapConfig = new Configuration($prefix); + $rawLdapConfig = $ldapConfig->getConfiguration(); + foreach ($rawLdapConfig as $key => $value) { + if (is_array($value)) { + $rawLdapConfig[$key] = implode(';', $value); + } + } + + $ldapConfigs[$prefix] = $rawLdapConfig; + } + + $this->initialState->provideInitialState('ldapConfigs', $ldapConfigs); + $this->initialState->provideInitialState('ldapModuleInstalled', function_exists('ldap_connect')); + return new TemplateResponse('user_ldap', 'settings', $parameters); } - /** - * @return string the section ID, e.g. 'sharing' - */ - public function getSection() { + public function getSection(): string { return 'ldap'; } @@ -74,7 +73,7 @@ class Admin implements IDelegatedSettings { * * E.g.: 70 */ - public function getPriority() { + public function getPriority(): int { return 5; } diff --git a/apps/user_ldap/openapi.json b/apps/user_ldap/openapi.json index 165aedced54..7a04c35072e 100644 --- a/apps/user_ldap/openapi.json +++ b/apps/user_ldap/openapi.json @@ -174,10 +174,10 @@ } }, "/ocs/v2.php/apps/user_ldap/api/v1/config/{configID}": { - "get": { - "operationId": "configapi-show", - "summary": "Get a configuration", - "description": "Output can look like this: ok 200 OK ldaps://my.ldap.server 7770 ou=small,dc=my,dc=ldap,dc=server ou=users,ou=small,dc=my,dc=ldap,dc=server ou=small,dc=my,dc=ldap,dc=server cn=root,dc=my,dc=ldap,dc=server clearTextWithShowPassword=1 1 0 displayname uid inetOrgPerson (&(objectclass=nextcloudUser)(nextcloudEnabled=TRUE)) 1 (&(|(objectclass=nextcloudGroup))) 0 nextcloudGroup cn memberUid (&(|(objectclass=inetOrgPerson))(uid=%uid)) 0 0 1 mail 20 auto auto 1 uid;sn;givenname 0 1 uid uid 0 0 500 1 \nThis endpoint requires admin access", + "delete": { + "operationId": "configapi-delete", + "summary": "Delete a LDAP configuration", + "description": "This endpoint requires admin access", "tags": [ "configapi" ], @@ -199,15 +199,6 @@ "type": "string" } }, - { - "name": "showPassword", - "in": "query", - "description": "Whether to show the password", - "schema": { - "type": "boolean", - "default": false - } - }, { "name": "OCS-APIRequest", "in": "header", @@ -221,7 +212,7 @@ ], "responses": { "200": { - "description": "Config returned", + "description": "Config deleted successfully", "content": { "application/json": { "schema": { @@ -240,12 +231,7 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } + "data": {} } } } @@ -418,7 +404,12 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": {} + "data": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } } } } @@ -540,10 +531,10 @@ } } }, - "delete": { - "operationId": "configapi-delete", - "summary": "Delete a LDAP configuration", - "description": "This endpoint requires admin access", + "get": { + "operationId": "configapi-show", + "summary": "Get a configuration", + "description": "Output can look like this: ok 200 OK ldaps://my.ldap.server 7770 ou=small,dc=my,dc=ldap,dc=server ou=users,ou=small,dc=my,dc=ldap,dc=server ou=small,dc=my,dc=ldap,dc=server cn=root,dc=my,dc=ldap,dc=server clearTextWithShowPassword=1 1 0 displayname uid inetOrgPerson (&(objectclass=nextcloudUser)(nextcloudEnabled=TRUE)) 1 (&(|(objectclass=nextcloudGroup))) 0 nextcloudGroup cn memberUid (&(|(objectclass=inetOrgPerson))(uid=%uid)) 0 0 1 mail 20 auto auto 1 uid;sn;givenname 0 1 uid uid 0 0 500 1 \nThis endpoint requires admin access", "tags": [ "configapi" ], @@ -565,6 +556,15 @@ "type": "string" } }, + { + "name": "showPassword", + "in": "query", + "description": "Whether to show the password", + "schema": { + "type": "boolean", + "default": false + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -578,7 +578,7 @@ ], "responses": { "200": { - "description": "Config deleted successfully", + "description": "Config returned", "content": { "application/json": { "schema": { @@ -597,7 +597,12 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": {} + "data": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } } } } diff --git a/apps/user_ldap/src/LDAPSettingsApp.vue b/apps/user_ldap/src/LDAPSettingsApp.vue new file mode 100644 index 00000000000..d2daeb62de6 --- /dev/null +++ b/apps/user_ldap/src/LDAPSettingsApp.vue @@ -0,0 +1,11 @@ + + + + diff --git a/apps/user_ldap/src/components/SettingsTabs/AdvancedTab.vue b/apps/user_ldap/src/components/SettingsTabs/AdvancedTab.vue new file mode 100644 index 00000000000..5ff05fa01bb --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/AdvancedTab.vue @@ -0,0 +1,294 @@ + + + + + diff --git a/apps/user_ldap/src/components/SettingsTabs/ExpertTab.vue b/apps/user_ldap/src/components/SettingsTabs/ExpertTab.vue new file mode 100644 index 00000000000..28695db1daf --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/ExpertTab.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/apps/user_ldap/src/components/SettingsTabs/GroupsTab.vue b/apps/user_ldap/src/components/SettingsTabs/GroupsTab.vue new file mode 100644 index 00000000000..fc35e28aafc --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/GroupsTab.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/apps/user_ldap/src/components/SettingsTabs/LoginTab.vue b/apps/user_ldap/src/components/SettingsTabs/LoginTab.vue new file mode 100644 index 00000000000..ab177182899 --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/LoginTab.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/apps/user_ldap/src/components/SettingsTabs/ServerTab.vue b/apps/user_ldap/src/components/SettingsTabs/ServerTab.vue new file mode 100644 index 00000000000..a382ee40d8f --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/ServerTab.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/apps/user_ldap/src/components/SettingsTabs/UsersTab.vue b/apps/user_ldap/src/components/SettingsTabs/UsersTab.vue new file mode 100644 index 00000000000..c2c2b9c61f7 --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/UsersTab.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/apps/user_ldap/src/components/WizardControls.vue b/apps/user_ldap/src/components/WizardControls.vue new file mode 100644 index 00000000000..a1100c55c81 --- /dev/null +++ b/apps/user_ldap/src/components/WizardControls.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/apps/user_ldap/src/main.ts b/apps/user_ldap/src/main.ts new file mode 100644 index 00000000000..4d22c5a8966 --- /dev/null +++ b/apps/user_ldap/src/main.ts @@ -0,0 +1,21 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import Vue from 'vue' +import { PiniaVuePlugin } from 'pinia' +import { getCSPNonce } from '@nextcloud/auth' + +import { pinia } from './store/index' +import LDAPSettingsApp from './LDAPSettingsApp.vue' + +__webpack_nonce__ = getCSPNonce() + +// Init Pinia store +Vue.use(PiniaVuePlugin) + +const LDAPSettingsAppVue = Vue.extend(LDAPSettingsApp) +new LDAPSettingsAppVue({ + name: 'LDAPSettingsApp', + pinia, +}).$mount('#content-ldap-settings') diff --git a/apps/user_ldap/src/models/index.ts b/apps/user_ldap/src/models/index.ts new file mode 100644 index 00000000000..6a3defea569 --- /dev/null +++ b/apps/user_ldap/src/models/index.ts @@ -0,0 +1,71 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export type LDAPConfig = { + ldapHost: string // Example: ldaps://my.ldap.server + ldapPort: string // Example: 7770 + ldapBackupHost: string + ldapBackupPort: string + ldapBase: string // Example: ou=small,dc=my,dc=ldap,dc=server + ldapBaseUsers: string // Example: ou=users,ou=small,dc=my,dc=ldap,dc=server + ldapBaseGroups: string // Example: ou=small,dc=my,dc=ldap,dc=server + ldapAgentName: string // Example: cn=root,dc=my,dc=ldap,dc=server + ldapAgentPassword: string // Example: clearTextWithShowPassword=1 + ldapTLS: '0'|'1' // Example: 1 + turnOffCertCheck: '0'|'1' // Example: 0 + ldapIgnoreNamingRules: string // Example: > + ldapUserDisplayName: string // Example: displayname + ldapUserDisplayName2: string // Example: uid + ldapUserFilterObjectclass?: string // Example: inetOrgPerson + ldapUserFilterGroups: string + ldapUserFilter: string // Example: (&(objectclass=nextcloudUser)(nextcloudEnabled=TRUE)) + ldapUserFilterMode: '0'|'1' // Example: 1 + ldapGroupFilter: string // Example: (&(|(objectclass=nextcloudGroup))) + ldapGroupFilterMode: '0'|'1' // Example: 0 + ldapGroupFilterObjectclass: string // Example: nextcloudGroup + ldapGroupFilterGroups: string + ldapGroupDisplayName: string // Example: cn + ldapGroupMemberAssocAttr: string // Example: memberUid + ldapLoginFilter: string // Example: (&(|(objectclass=inetOrgPerson))(uid=%uid)) + ldapLoginFilterMode: '0'|'1' // Example: 0 + ldapLoginFilterEmail: '0'|'1' // Example: 0 + ldapLoginFilterUsername: '0'|'1' // Example: 1 + ldapLoginFilterAttributes: string + ldapQuotaAttribute: string + ldapQuotaDefault: string + ldapEmailAttribute: string // Example: mail + ldapCacheTTL: string // Example: 20 + ldapUuidUserAttribute: string // Example: auto + ldapUuidGroupAttribute: string // Example: auto + ldapOverrideMainServer: string + ldapConfigurationActive: '0'|'1' // Example: 1 + ldapAttributesForUserSearch: string // Example: uid;sn;givenname + ldapAttributesForGroupSearch: string + ldapExperiencedAdmin: '0'|'1' // Example: 0 + homeFolderNamingRule: string + hasMemberOfFilterSupport: string + useMemberOfToDetectMembership: '0'|'1' // Example: 1 + ldapExpertUsernameAttr: string // Example: uid + ldapExpertUUIDUserAttr: string // Example: uid + ldapExpertUUIDGroupAttr: string + lastJpegPhotoLookup: '0'|'1' // Example: 0 + ldapNestedGroups: '0'|'1' // Example: 0 + ldapPagingSize: string // Example: 500 + turnOnPasswordChange: '0'|'1' // Example: 1 + ldapDynamicGroupMemberURL: string + markRemnantsAsDisabled: '0'|'1' // Example: 1 + ldapDefaultPPolicyDN: string + ldapExtStorageHomeAttribute: string + ldapAttributePhone: string + ldapAttributeWebsite: string + ldapAttributeAddress: string + ldapAttributeTwitter: string + ldapAttributeFediverse: string + ldapAttributeOrganisation: string + ldapAttributeRole: string + ldapAttributeHeadline: string + ldapAttributeBiography: string + ldapAttributeBirthDate: string +} diff --git a/apps/user_ldap/src/services/ldapConfigService.ts b/apps/user_ldap/src/services/ldapConfigService.ts new file mode 100644 index 00000000000..809b063ea5b --- /dev/null +++ b/apps/user_ldap/src/services/ldapConfigService.ts @@ -0,0 +1,189 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import path from 'path' + +import { DialogSeverity, getDialogBuilder, showError, showSuccess } from '@nextcloud/dialogs' +import axios, { AxiosError, type AxiosResponse } from '@nextcloud/axios' +import { getAppRootUrl, generateOcsUrl } from '@nextcloud/router' +import type { OCSResponse } from '@nextcloud/typings/ocs' +import { t } from '@nextcloud/l10n' + +import type { LDAPConfig } from '../models' +import logger from './logger' + +const AJAX_ENDPOINT = path.join(getAppRootUrl('user_ldap'), '/ajax') + +export type WizardAction = + 'guessPortAndTLS' | + 'guessBaseDN' | + 'detectEmailAttribute' | + 'detectUserDisplayNameAttribute' | + 'determineGroupMemberAssoc' | + 'determineUserObjectClasses' | + 'determineGroupObjectClasses' | + 'determineGroupsForUsers' | + 'determineGroupsForGroups' | + 'determineAttributes' | + 'getUserListFilter' | + 'getUserLoginFilter' | + 'getGroupFilter' | + 'countUsers' | + 'countGroups' | + 'countInBaseDN' | + 'testLoginName' | + 'save' + +export async function createConfig() { + const response = await axios.post(generateOcsUrl('apps/user_ldap/api/v1/config')) as AxiosResponse> + logger.debug('Created configuration', { configId: response.data.ocs.data.configID }) + return response.data.ocs.data.configID +} + +export async function copyConfig(configId: string) { + const params = new FormData() + params.set('copyConfig', configId) + + const response = await axios.post( + path.join(AJAX_ENDPOINT, 'getNewServerConfigPrefix.php'), + params, + ) as AxiosResponse<{status: 'error'|'success', configPrefix: string}> + + logger.debug('Created configuration', { configId: response.data.configPrefix }) + return response.data.configPrefix +} + +export async function getConfig(configId: string): Promise { + const response = await axios.get(generateOcsUrl('apps/user_ldap/api/v1/config/{configId}', { configId })) as AxiosResponse> + logger.debug('Fetched configuration', { configId, config: response.data.ocs.data }) + return response.data.ocs.data +} + +export async function updateConfig(configId: string, config: LDAPConfig): Promise { + const response = await axios.put( + generateOcsUrl('apps/user_ldap/api/v1/config/{configId}', { configId }), + { configData: config }, + ) as AxiosResponse> + + logger.debug('Updated configuration', { configId, config }) + + return response.data.ocs.data +} + +export async function deleteConfig(configId: string): Promise { + try { + const isConfirmed = await confirmOperation( + t('user_ldap', 'Confirm action'), + t('user_ldap', 'Are you sure you want to permanently delete this LDAP configuration? This cannot be undone.'), + ) + if (!isConfirmed) { + return false + } + + await axios.delete(generateOcsUrl('apps/user_ldap/api/v1/config/{configId}', { configId })) + logger.debug('Deleted configuration', { configId }) + } catch (error) { + const errorResponse = (error as AxiosError).response + showError(errorResponse?.data.ocs.meta.message || t('user_ldap', 'Fail to delete config')) + } + + return true +} + +export async function testConfiguration(configId: string) { + const params = new FormData() + params.set('ldap_serverconfig_chooser', configId) + + const response = await axios.post( + path.join(AJAX_ENDPOINT, 'testConfiguration.php'), + params, + ) as AxiosResponse<{message: string, status: 'error'|'success'}> + + logger.debug(`Configuration is ${response.data.status === 'success' ? 'valide' : 'invalide'}`, { configId, params, response }) + + return response.data +} + +export async function clearMapping(subject: 'user' | 'group') { + const isConfirmed = await confirmOperation( + t('user_ldap', 'Confirm action'), + t('user_ldap', 'Are you sure you want to permanently clear the LDAP mapping? This cannot be undone.'), + ) + if (!isConfirmed) { + return false + } + + const params = new FormData() + params.set('ldap_clear_mapping', subject) + + const response = await axios.post( + path.join(AJAX_ENDPOINT, 'clearMappings.php'), + params, + ) + + if (response.data.status === 'success') { + logger.debug('Cleared mapping', { subject, params, response }) + showSuccess(t('user_ldap', 'Mapping cleared')) + } else { + showError(t('user_ldap', 'Failed to clear mapping')) + } +} + +export async function callWizard(action: WizardAction, configId: string, extraParams: Record = {}) { + const params = new FormData() + params.set('action', action) + params.set('ldap_serverconfig_chooser', configId) + + Object.entries(extraParams).forEach(([key, value]) => { + params.set(key, value) + }) + + const response = await axios.post( + path.join(AJAX_ENDPOINT, 'wizard.php'), + params, + ) as AxiosResponse<{ status: 'error', message?: string} | {status: 'success', changes?: Record, options?: Record}> + + logger.debug(`Called wizard action: ${action}`, { configId, params, response }) + + if (response.data.status === 'error') { + const message = response.data.message ?? t('user_ldap', 'An error occurred') + showError(message) + throw new Error(message) + } + + return response.data +} + +export async function showEnableAutomaticFilterInfo() { + return await confirmOperation( + t('user_ldap', 'Mode switch'), + t('user_ldap', 'Switching the mode will enable automatic LDAP queries. Depending on your LDAP size they may take a while. Do you still want to switch the mode?'), + ) +} + +export async function confirmOperation(name: string, text: string): Promise { + return new Promise((resolve) => { + const dialog = getDialogBuilder(name) + .setText(text) + .setSeverity(DialogSeverity.Warning) + .addButton({ + label: t('user_ldap', 'Cancel'), + callback() { + dialog.hide() + resolve(false) + }, + }) + .addButton({ + label: t('user_ldap', 'Confirm'), + variant: 'error', + callback() { + resolve(true) + }, + }) + .build() + + dialog.show() + }) +} diff --git a/apps/user_ldap/src/services/logger.ts b/apps/user_ldap/src/services/logger.ts new file mode 100644 index 00000000000..cb66efa8b81 --- /dev/null +++ b/apps/user_ldap/src/services/logger.ts @@ -0,0 +1,11 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getLoggerBuilder } from '@nextcloud/logger' + +export default getLoggerBuilder() + .setApp('LDAP') + .detectUser() + .build() diff --git a/apps/user_ldap/src/store/configs.ts b/apps/user_ldap/src/store/configs.ts new file mode 100644 index 00000000000..6729217d094 --- /dev/null +++ b/apps/user_ldap/src/store/configs.ts @@ -0,0 +1,75 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { defineStore } from 'pinia' +import Vue, { computed, ref } from 'vue' + +import { loadState } from '@nextcloud/initial-state' + +import { callWizard, copyConfig, createConfig, deleteConfig, getConfig } from '../services/ldapConfigService' +import type { LDAPConfig } from '../models' + +export const useLDAPConfigsStore = defineStore('ldap-configs', () => { + const ldapConfigs = ref(loadState('user_ldap', 'ldapConfigs') as Record) + const selectedConfigId = ref(Object.keys(ldapConfigs.value)[0]) + const selectedConfig = computed(() => ldapConfigs.value[selectedConfigId.value]) + const updatingConfig = ref(0) + + function getConfigProxy(configId: string, postSetHooks: Partial void >> = {}) { + return new Proxy(ldapConfigs.value[configId], { + get(target, property) { + return target[property] + }, + set(target, property: string, newValue) { + target[property] = newValue + + ;(async () => { + updatingConfig.value++ + await callWizard('save', configId, { cfgkey: property, cfgval: newValue }) + updatingConfig.value-- + + if (postSetHooks[property] !== undefined) { + postSetHooks[property](target[property]) + } + })() + + return true + }, + }) + } + + async function create() { + const configId = await createConfig() + Vue.set(ldapConfigs.value, configId, await getConfig(configId)) + selectedConfigId.value = configId + return configId + } + + async function _copyConfig(fromConfigId: string) { + const configId = await copyConfig(fromConfigId) + Vue.set(ldapConfigs.value, configId, { ...ldapConfigs.value[fromConfigId] }) + selectedConfigId.value = configId + return configId + } + + async function removeConfig(configId: string) { + const result = await deleteConfig(configId) + if (result === true) { + Vue.delete(ldapConfigs.value, configId) + } + + selectedConfigId.value = Object.keys(ldapConfigs.value)[0] ?? await create() + } + + return { + ldapConfigs, + selectedConfigId, + selectedConfig, + updatingConfig, + getConfigProxy, + create, + copyConfig: _copyConfig, + removeConfig, + } +}) diff --git a/apps/user_ldap/src/store/index.ts b/apps/user_ldap/src/store/index.ts new file mode 100644 index 00000000000..00676b3bc8e --- /dev/null +++ b/apps/user_ldap/src/store/index.ts @@ -0,0 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createPinia } from 'pinia' + +export const pinia = createPinia() diff --git a/apps/user_ldap/src/views/Settings.vue b/apps/user_ldap/src/views/Settings.vue new file mode 100644 index 00000000000..88a3d014da8 --- /dev/null +++ b/apps/user_ldap/src/views/Settings.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/apps/user_ldap/templates/settings.php b/apps/user_ldap/templates/settings.php index 9117a9f533c..aad2053fbc2 100644 --- a/apps/user_ldap/templates/settings.php +++ b/apps/user_ldap/templates/settings.php @@ -49,7 +49,8 @@ script('user_ldap', [ 'wizard/wizardDetectorClearGroupMappings', 'wizard/wizardFilterOnType', 'wizard/wizardFilterOnTypeFactory', - 'wizard/wizard' + 'wizard/wizard', + 'main' ]); style('user_ldap', 'settings'); @@ -161,3 +162,5 @@ style('user_ldap', 'settings'); + +
diff --git a/apps/user_ldap/tests/Settings/AdminTest.php b/apps/user_ldap/tests/Settings/AdminTest.php index b17e96c1a68..eb6d4baa26e 100644 --- a/apps/user_ldap/tests/Settings/AdminTest.php +++ b/apps/user_ldap/tests/Settings/AdminTest.php @@ -10,6 +10,7 @@ namespace OCA\User_LDAP\Tests\Settings; use OCA\User_LDAP\Configuration; use OCA\User_LDAP\Settings\Admin; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\IL10N; use OCP\Server; use OCP\Template\ITemplateManager; @@ -23,16 +24,19 @@ use Test\TestCase; class AdminTest extends TestCase { private IL10N&MockObject $l10n; private ITemplateManager $templateManager; + private IInitialState&MockObject $initialState; private Admin $admin; protected function setUp(): void { parent::setUp(); $this->l10n = $this->createMock(IL10N::class); $this->templateManager = Server::get(ITemplateManager::class); + $this->initialState = $this->createMock(IInitialState::class); $this->admin = new Admin( $this->l10n, $this->templateManager, + $this->initialState, ); } @@ -46,10 +50,6 @@ class AdminTest extends TestCase { $sControls = $sControls->fetchPage(); $parameters = []; - $parameters['serverConfigurationPrefixes'] = $prefixes; - $parameters['serverConfigurationHosts'] = $hosts; - $parameters['settingControls'] = $sControls; - $parameters['wizardControls'] = $wControls; // assign default values $config = new Configuration('', false); diff --git a/openapi.json b/openapi.json index 1a777cb7263..6b9812a17e8 100644 --- a/openapi.json +++ b/openapi.json @@ -34429,10 +34429,10 @@ } }, "/ocs/v2.php/apps/user_ldap/api/v1/config/{configID}": { - "get": { - "operationId": "user_ldap-configapi-show", - "summary": "Get a configuration", - "description": "Output can look like this: ok 200 OK ldaps://my.ldap.server 7770 ou=small,dc=my,dc=ldap,dc=server ou=users,ou=small,dc=my,dc=ldap,dc=server ou=small,dc=my,dc=ldap,dc=server cn=root,dc=my,dc=ldap,dc=server clearTextWithShowPassword=1 1 0 displayname uid inetOrgPerson (&(objectclass=nextcloudUser)(nextcloudEnabled=TRUE)) 1 (&(|(objectclass=nextcloudGroup))) 0 nextcloudGroup cn memberUid (&(|(objectclass=inetOrgPerson))(uid=%uid)) 0 0 1 mail 20 auto auto 1 uid;sn;givenname 0 1 uid uid 0 0 500 1 \nThis endpoint requires admin access", + "delete": { + "operationId": "user_ldap-configapi-delete", + "summary": "Delete a LDAP configuration", + "description": "This endpoint requires admin access", "tags": [ "user_ldap/configapi" ], @@ -34454,15 +34454,6 @@ "type": "string" } }, - { - "name": "showPassword", - "in": "query", - "description": "Whether to show the password", - "schema": { - "type": "boolean", - "default": false - } - }, { "name": "OCS-APIRequest", "in": "header", @@ -34476,7 +34467,7 @@ ], "responses": { "200": { - "description": "Config returned", + "description": "Config deleted successfully", "content": { "application/json": { "schema": { @@ -34495,12 +34486,7 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } + "data": {} } } } @@ -34673,7 +34659,12 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": {} + "data": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } } } } @@ -34795,10 +34786,10 @@ } } }, - "delete": { - "operationId": "user_ldap-configapi-delete", - "summary": "Delete a LDAP configuration", - "description": "This endpoint requires admin access", + "get": { + "operationId": "user_ldap-configapi-show", + "summary": "Get a configuration", + "description": "Output can look like this: ok 200 OK ldaps://my.ldap.server 7770 ou=small,dc=my,dc=ldap,dc=server ou=users,ou=small,dc=my,dc=ldap,dc=server ou=small,dc=my,dc=ldap,dc=server cn=root,dc=my,dc=ldap,dc=server clearTextWithShowPassword=1 1 0 displayname uid inetOrgPerson (&(objectclass=nextcloudUser)(nextcloudEnabled=TRUE)) 1 (&(|(objectclass=nextcloudGroup))) 0 nextcloudGroup cn memberUid (&(|(objectclass=inetOrgPerson))(uid=%uid)) 0 0 1 mail 20 auto auto 1 uid;sn;givenname 0 1 uid uid 0 0 500 1 \nThis endpoint requires admin access", "tags": [ "user_ldap/configapi" ], @@ -34820,6 +34811,15 @@ "type": "string" } }, + { + "name": "showPassword", + "in": "query", + "description": "Whether to show the password", + "schema": { + "type": "boolean", + "default": false + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -34833,7 +34833,7 @@ ], "responses": { "200": { - "description": "Config deleted successfully", + "description": "Config returned", "content": { "application/json": { "schema": { @@ -34852,7 +34852,12 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": {} + "data": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } } } } diff --git a/webpack.modules.js b/webpack.modules.js index a15c0e1c54f..f79873f3c9d 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -117,6 +117,9 @@ module.exports = { updatenotification: path.join(__dirname, 'apps/updatenotification/src', 'updatenotification.js'), 'update-notification-legacy': path.join(__dirname, 'apps/updatenotification/src', 'update-notification-legacy.ts'), }, + user_ldap: { + main: path.join(__dirname, 'apps/user_ldap/src', 'main.js'), + }, user_status: { menu: path.join(__dirname, 'apps/user_status/src', 'menu.js'), },