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 @@ + + + + + {{ t('user_ldap', 'Connection Settings') }} + + + {{ t('user_ldap', 'Configuration Active') }} + + + + + + + + {{ t('user_ldap', 'Disable Main Server') }} + + + + {{ t('user_ldap', 'Turn off SSL certificate validation.') }} + + + + + + + {{ t('user_ldap', 'Directory Settings') }} + + + + + + + + + + + {{ t('user_ldap', 'Disable users missing from LDAP') }} + + + + + + + + + + + + + + + {{ t('user_ldap', 'Nested Groups') }} + + + + + + {{ t('user_ldap', 'Enable LDAP password changes per user') }} + + + {{ t('user_ldap', '(New password is sent as plain text to LDAP)') }} + + + + + + + {{ t('user_ldap', 'Special Attributes') }} + + + + + + + + + + + + + + {{ t('user_ldap', 'User Profile Attributes') }} + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + {{ t('user_ldap', 'Internal Username') }} + {{ t('user_ldap', 'By default the internal username will be created from the UUID attribute. It makes sure that the username is unique and characters do not need to be converted. The internal username has the restriction that only these characters are allowed: [a-zA-Z0-9_.@-]. Other characters are replaced with their ASCII correspondence or simply omitted. On collisions a number will be added/increased. The internal username is used to identify a user internally. It is also the default name for the user home folder. It is also a part of remote URLs, for instance for all DAV services. With this setting, the default behavior can be overridden. Changes will have effect only on newly mapped (added) LDAP users. Leave it empty for default behavior.') }} + + + + + {{ t('user_ldap', 'Override UUID detection') }} + {{ t('user_ldap', 'By default, the UUID attribute is automatically detected. The UUID attribute is used to doubtlessly identify LDAP users and groups. Also, the internal username will be created based on the UUID, if not specified otherwise above. You can override the setting and pass an attribute of your choice. You must make sure that the attribute of your choice can be fetched for both users and groups and it is unique. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users and groups.') }} + + + + + + + + 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 @@ + + + + {{ t('user_ldap', 'Groups meeting these criteria are available in {instanceName}:', {instanceName}) }} + + + + + + + + + + + + + + + {{ t('user_ldap', 'Available groups') }} + + + + > + < + + + + + {{ t('user_ldap', 'Selected groups') }} + + + + + + {{ t('user_name', 'Edit LDAP Query') }} + + + + {{ t('user_name', 'LDAP Filter:') }} + {{ ldapConfig.ldapGroupFilter }} + + + + + + + + + {{ t('user_ldap', 'Verify settings and count the groups') }} + + + {{ t('user_ldap', "Groups count: {groupsCount}", { groupsCount }) }} + + + + + + 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 @@ + + + + {{ t('user_ldap', 'When logging in, {instanceName} will find the user based on the following attributes:', { instanceName }) }} + + + + {{ t('user_ldap', 'LDAP/AD Username') }} + + + + {{ t('user_ldap', 'LDAP/AD Email Address') }} + + + + + + + + {{ t('user_name', 'Edit LDAP Query') }} + + + + {{ t('user_name', 'LDAP Filter:') }} + {{ ldapConfig.ldapLoginFilter }} + + + + + + + + + + + {{ t('user_ldap', 'Verify settings') }} + + + + + + + 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 @@ + + + + + ldapConfigsStore.copyConfig(ldapConfigId)"> + + + + + ldapConfigsStore.removeConfig(ldapConfigId)"> + + + + + + + + + + + + {{ t('user_ldap', 'Detect Port') }} + + + + + + + + + + + + + {{ t('user_ldap', 'Save Credentials') }} + + + + + + + + {{ t('user_ldap', 'Detect Base DN') }} + + + {{ t('user_ldap', 'Test Base DN') }} + + + + + + {{ t('user_ldap', 'Manually enter LDAP filters (recommended for large directories)') }} + + + + + + + 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 @@ + + + + {{ t('user_name', 'Listing and searching for users is constrained by these criteria:') }} + + + + {{ t('user_name', 'The most common object classes for users are organizationalPerson, person, user, and inetOrgPerson. If you are not sure which object class to select, please consult your directory admin.') }} + + + + + {{ t('user_name', 'Only from these groups:') }} + + + + + + + + + + + + + + + {{ t('user_name', 'Available groups') }} + + + + > + < + + + + + + {{ t('user_name', 'Selected groups') }} + + + + + + {{ t('user_name', 'Edit LDAP Query') }} + + + + {{ t('user_name', 'LDAP Filter:') }} + {{ ldapConfig.ldapUserFilter }} + + + + + + + + + {{ t('user_name', 'Verify settings and count users') }} + + + {{ t('user_ldap', "User count: {userCount}", { usersCount }) }} + + + + + + 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 @@ + + + + + + + + + + + {{ t('user_ldap', 'Help') }} + + + + {{ t('user_ldap', 'Test Configuration') }} + + + + + + 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 @@ + + + + {{ t('user_ldap', 'LDAP/AD integration') }} + + + + + {{ `${configId}: ${ldapConfigs[configId].ldapHost}` }} + + + {{ `${configId}: ${ldapConfigs[configId].ldapHost}` }} + + + + + + + + + + + + + + + + {{ tabLabel }} + + + + + {{ tabLabel }} + + + + + + + + + + + + + + + + {{ t('user_ldap', 'Username-LDAP User Mapping') }} + {{ t('user_ldap', 'Usernames are used to store and assign metadata. In order to precisely identify and recognize users, each LDAP user will have an internal username. This requires a mapping from username to LDAP user. The created username is mapped to the UUID of the LDAP user. Additionally the DN is cached as well to reduce LDAP interaction, but it is not used for identification. If the DN changes, the changes will be found. The internal username is used all over. Clearing the mappings will have leftovers everywhere. Clearing the mappings is not configuration sensitive, it affects all LDAP configurations! Never clear the mappings in a production environment, only in a testing or experimental stage.') }} + + + + {{ t('user_ldap', 'Clear Username-LDAP User Mapping') }} + + + {{ t('user_ldap', 'Clear Groupname-LDAP Group Mapping') }} + + + + + + + + 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'), },
+ +
+ {{ t('user_ldap', 'Available groups') }} +
+ {{ t('user_ldap', 'Selected groups') }} +
{{ ldapConfig.ldapGroupFilter }}
{{ ldapConfig.ldapLoginFilter }}
+ {{ t('user_name', 'Available groups') }} +
+ {{ t('user_name', 'Selected groups') }} +
{{ ldapConfig.ldapUserFilter }}