diff --git a/apps/user_ldap/appinfo/routes.php b/apps/user_ldap/appinfo/routes.php index fae78b8982e30..a0e3ee1d4e2f3 100644 --- a/apps/user_ldap/appinfo/routes.php +++ b/apps/user_ldap/appinfo/routes.php @@ -23,14 +23,6 @@ ->actionInclude('user_ldap/ajax/wizard.php'); $application = new \OCP\AppFramework\App('user_ldap'); -$application->registerRoutes($this, [ - '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'], - ] -]); /** @var \OCA\User_LDAP\AppInfo\Application $application */ $application = \OC::$server->query(\OCA\User_LDAP\AppInfo\Application::class); diff --git a/apps/user_ldap/lib/Controller/ConfigAPIController.php b/apps/user_ldap/lib/Controller/ConfigAPIController.php index 8ce2486c153ab..336bfc47524e3 100644 --- a/apps/user_ldap/lib/Controller/ConfigAPIController.php +++ b/apps/user_ldap/lib/Controller/ConfigAPIController.php @@ -12,7 +12,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; @@ -57,6 +57,7 @@ public function __construct( * 200: Config created successfully */ #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'POST', url: '/api/v1/config')] public function create() { try { $configPrefix = $this->ldapHelper->getNextServerConfigurationPrefix(); @@ -81,6 +82,7 @@ public function create() { * 200: Config deleted successfully */ #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'DELETE', url: '/api/v1/config/{configID}')] public function delete($configID) { try { $this->ensureConfigIDExists($configID); @@ -110,6 +112,7 @@ public function delete($configID) { * 200: Config returned */ #[AuthorizedAdminSetting(settings: Admin::class)] + #[ApiRoute(verb: 'PUT', url: '/api/v1/config/{configID}')] public function modify($configID, $configData) { try { $this->ensureConfigIDExists($configID); @@ -214,6 +217,7 @@ public function modify($configID, $configData) { * 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 8fc0915c6fe42..c609a9e48de0a 100644 --- a/apps/user_ldap/lib/Settings/Admin.php +++ b/apps/user_ldap/lib/Settings/Admin.php @@ -8,19 +8,16 @@ 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\Settings\IDelegatedSettings; use OCP\Template; class Admin implements IDelegatedSettings { - /** @var IL10N */ - private $l; - - /** - * @param IL10N $l - */ - public function __construct(IL10N $l) { - $this->l = $l; + public function __construct( + private IL10N $l, + private IInitialState $InitialState + ) { } /** @@ -44,6 +41,7 @@ public function getForm() { $sControls = new Template('user_ldap', 'part.settingcontrols'); $sControls = $sControls->fetchPage(); + // TODO: remove $parameters['serverConfigurationPrefixes'] = $prefixes; $parameters['serverConfigurationHosts'] = $hosts; $parameters['settingControls'] = $sControls; @@ -58,6 +56,22 @@ public function getForm() { $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); } diff --git a/apps/user_ldap/src/LDAPSettingsApp.vue b/apps/user_ldap/src/LDAPSettingsApp.vue new file mode 100644 index 0000000000000..1541566e4aa15 --- /dev/null +++ b/apps/user_ldap/src/LDAPSettingsApp.vue @@ -0,0 +1,21 @@ + + + + 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 0000000000000..70635d76c527b --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/AdvancedTab.vue @@ -0,0 +1,272 @@ + + + + + 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 0000000000000..b1896208cadfc --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/ExpertTab.vue @@ -0,0 +1,55 @@ + + + + + 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 0000000000000..c99e4f763437e --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/GroupsTab.vue @@ -0,0 +1,152 @@ + + + + + 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 0000000000000..7c0a7ca2f1407 --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/LoginTab.vue @@ -0,0 +1,175 @@ + + + + + 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 0000000000000..f785dbe38c6f3 --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/ServerTab.vue @@ -0,0 +1,162 @@ + + + + + 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 0000000000000..44c2f6264432b --- /dev/null +++ b/apps/user_ldap/src/components/SettingsTabs/UsersTab.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/apps/user_ldap/src/components/WizardControls.vue b/apps/user_ldap/src/components/WizardControls.vue new file mode 100644 index 0000000000000..ec655a37c8083 --- /dev/null +++ b/apps/user_ldap/src/components/WizardControls.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/apps/user_ldap/src/main.ts b/apps/user_ldap/src/main.ts new file mode 100644 index 0000000000000..b6f285cbf99bc --- /dev/null +++ b/apps/user_ldap/src/main.ts @@ -0,0 +1,20 @@ +/** + * 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({ + 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 0000000000000..632b43e44e842 --- /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 0000000000000..a7ac3eae9f533 --- /dev/null +++ b/apps/user_ldap/src/services/ldapConfigService.ts @@ -0,0 +1,181 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import path from 'path' + +import axios, { AxiosError, type AxiosResponse } from '@nextcloud/axios' +import { getAppRootUrl, generateOcsUrl } from '@nextcloud/router' + +import type { LDAPConfig } from '../models' +import { DialogSeverity, getDialogBuilder, showError, showSuccess } from '@nextcloud/dialogs' +import type { OCSResponse } from '@nextcloud/typings/ocs' +import { t } from '@nextcloud/l10n' + +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' + +/** + * + * @param config + */ +export async function createConfig() { + const response = await axios.post(generateOcsUrl('apps/user_ldap/api/v1/config')) + return response.data.ocs.data.configID as string +} + +/** + * + * @param configId + * @param config + */ +export async function getConfig(configId: string): Promise { + const response: AxiosResponse> = await axios.get(generateOcsUrl('apps/user_ldap/api/v1/config/{configId}', { configId })) + return response.data.ocs.data +} + +/** + * + * @param configId + * @param config + */ +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 }, + ) + + return response.data as LDAPConfig +} + +/** + * + * @param configId + */ +export async function deleteConfig(configId: string): Promise { + try { + await axios.delete(generateOcsUrl('apps/user_ldap/api/v1/config/{configId}', { configId })) + } catch (error) { + const errorResponse = (error as AxiosError).response + showError(errorResponse?.data.ocs.meta.message || t('user_ldap', 'Fail to delete config')) + } + + return true +} + +/** + * Starts a configuration test. + * @param configId + */ +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, + ) + + if (response.data.status === 'success') { + showSuccess(response.data.message) + } else { + showError(response.data.message) + } + + return response.data +} + +/** + * + * @param subject + */ +export async function clearMapping(subject: 'user' | 'group') { + 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') { + showSuccess(t('user_ldap', 'Mapping cleared')) + } else { + showError(t('user_ldap', 'Failed to clear mapping')) + } +} + +/** + * Calls the wizard endpoint. + * @param action + * @param configId + * @param extraParams + */ +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, + ) + + if (response.data.status === 'error') { + showError(response.data.message) + throw new Error(response.data.message) + } + + return response.data +} + +/** + * + * @param value + */ +export async function showEnableAutomaticFilterInfo(): Promise<'0'|'1'> { + return new Promise((resolve) => { + const dialog = getDialogBuilder(t('user_ldap', 'Mode switch')) + .setText(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?')) + .addButton({ + label: t('user_ldap', 'No'), + callback() { + dialog.hide() + resolve('1') + }, + }) + .addButton({ + label: t('user_ldap', 'Yes'), + callback() { + resolve('0') + }, + }) + .setSeverity(DialogSeverity.Info) + .build() + + dialog.show() + }) +} diff --git a/apps/user_ldap/src/store/configs.ts b/apps/user_ldap/src/store/configs.ts new file mode 100644 index 0000000000000..49d0205f2a7b3 --- /dev/null +++ b/apps/user_ldap/src/store/configs.ts @@ -0,0 +1,62 @@ +/** + * 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 { 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]) + + /** + * + */ + async function create() { + const configId = await createConfig() + const config = await getConfig(configId) + ldapConfigs.value[configId] = config + selectedConfigId.value = configId + return configId + } + + /** + * + * @param fromConfigId + */ + async function copyConfig(fromConfigId: string) { + const configId = await createConfig() + ldapConfigs.value[configId] = { ...ldapConfigs.value[fromConfigId] } + selectedConfigId.value = configId + return configId + } + + /** + * + * @param configId + */ + async function removeConfig(configId: string) { + const result = await deleteConfig(configId) + if (result === true) { + Vue.delete(ldapConfigs.value, configId) + } + + const firstConfigId = Object.keys(ldapConfigs.value)[0] ?? await create() + selectedConfigId.value = firstConfigId + } + + return { + ldapConfigs, + selectedConfigId, + selectedConfig, + create, + 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 0000000000000..00676b3bc8eda --- /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/store/wizard.ts b/apps/user_ldap/src/store/wizard.ts new file mode 100644 index 0000000000000..f277ae7c0ae25 --- /dev/null +++ b/apps/user_ldap/src/store/wizard.ts @@ -0,0 +1,34 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +import { callWizard, type WizardAction } from '../services/ldapConfigService' +import { useLDAPConfigsStore } from './configs' + +export const useWizardStore = defineStore('ldap-wizard', () => { + const currentWizardActions = ref([]) + + const { selectedConfigId } = useLDAPConfigsStore() + + /** + * + * @param action + * @param params + */ + async function callWizardAction(action: WizardAction, params?: Record) { + try { + currentWizardActions.value.push(action) + return await callWizard(action, selectedConfigId, params) + } finally { + currentWizardActions.value.splice(currentWizardActions.value.indexOf(action), 1) + } + } + + return { + currentWizardActions, + callWizardAction, + } +}) diff --git a/apps/user_ldap/src/views/Settings.vue b/apps/user_ldap/src/views/Settings.vue new file mode 100644 index 0000000000000..915b53c52e708 --- /dev/null +++ b/apps/user_ldap/src/views/Settings.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/apps/user_ldap/templates/settings.php b/apps/user_ldap/templates/settings.php index 9117a9f533caa..aad2053fbc26b 100644 --- a/apps/user_ldap/templates/settings.php +++ b/apps/user_ldap/templates/settings.php @@ -49,7 +49,8 @@ 'wizard/wizardDetectorClearGroupMappings', 'wizard/wizardFilterOnType', 'wizard/wizardFilterOnTypeFactory', - 'wizard/wizard' + 'wizard/wizard', + 'main' ]); style('user_ldap', 'settings'); @@ -161,3 +162,5 @@ + +
diff --git a/package.json b/package.json index 07714e596cbfa..3f7c0d06318a7 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "postbuild": "build/npm-post-build.sh", "dev": "webpack --node-env development --progress", "watch": "webpack --node-env development --progress --watch", + "serve": "webpack serve --node-env development --progress", "lint": "eslint $(for appdir in $(ls apps); do if ! $(git check-ignore -q $appdir); then printf \"apps/$appdir \"; fi; done) core --no-error-on-unmatched-pattern", "lint:fix": "eslint $(for appdir in $(ls apps); do if ! $(git check-ignore -q $appdir); then printf \"apps/$appdir \"; fi; done) core --no-error-on-unmatched-pattern --fix", "stylelint": "stylelint '{apps,core}/**/*.{scss,vue}'", @@ -205,4 +206,4 @@ "overrides": { "colors": "1.4.0" } -} +} \ No newline at end of file diff --git a/webpack.modules.js b/webpack.modules.js index f788e6f6e84b9..79caaac13e3ab 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -109,6 +109,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'), },