From 45af1f3d8bf50d82aa78564f3cbf257f1852cabd Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 22 Sep 2021 10:11:53 +0300 Subject: [PATCH] refactor(settings): backport auth views (#5672) --- .../authentication/auth-method-constants.js | 11 + .../authentication/auth-type-constants.js | 11 + .../auto-team-membership-toggle.html | 14 + .../auto-team-membership-toggle/index.js | 9 + .../auto-user-provision-toggle.html | 14 + .../auto-user-provision-toggle/index.js | 9 + .../settings/authentication/index.js | 12 + .../ad-settings/ad-settings.controller.js | 61 +++ .../ldap/ad-settings/ad-settings.html | 115 +++++ .../authentication/ldap/ad-settings/index.js | 12 + .../settings/authentication/ldap/index.js | 44 ++ .../ldap/ldap-connectivity-check/index.js | 8 + .../ldap-connectivity-check.html | 19 + .../ldap/ldap-custom-group-search/index.js | 10 + .../ldap-custom-group-search.controller.js | 34 ++ .../ldap-custom-group-search.html | 64 +++ .../ldap/ldap-custom-user-search/index.js | 10 + .../ldap-custom-user-search.controller.js | 33 ++ .../ldap-custom-user-search.html | 64 +++ .../ldap/ldap-group-search-item/index.js | 14 + .../ldap-group-search-item.controller.js | 51 +++ .../ldap-group-search-item.html | 68 +++ .../ldap/ldap-group-search/index.js | 13 + .../ldap-group-search.controller.js | 36 ++ .../ldap-group-search/ldap-group-search.html | 32 ++ .../ldap/ldap-groups-datatable/index.js | 12 + .../ldap-groups-datatable.html | 77 ++++ .../ldap/ldap-settings-custom/index.js | 15 + .../ldap-settings-custom.controller.js | 9 + .../ldap-settings-custom.html | 99 +++++ .../ldap/ldap-settings-dn-builder/index.js | 15 + .../ldap-settings-dn-builder.controller.js | 84 ++++ .../ldap-settings-dn-builder.html | 36 ++ .../ldap-settings-group-dn-builder/index.js | 17 + ...ap-settings-group-dn-builder.controller.js | 55 +++ .../ldap-settings-group-dn-builder.html | 21 + .../ldap/ldap-settings-openldap/index.js | 16 + .../ldap-settings-openldap.controller.js | 42 ++ .../ldap-settings-openldap.html | 129 ++++++ .../ldap/ldap-settings-security/index.js | 10 + .../ldap-settings-security.html | 57 +++ .../ldap/ldap-settings-test-login/index.js | 9 + .../ldap-settings-test-login.controller.js | 31 ++ .../ldap-settings-test-login.html | 32 ++ .../ldap/ldap-settings.model.js | 54 +++ .../ldap/ldap-settings/index.js | 11 + .../ldap-settings/ldap-settings.controller.js | 66 +++ .../ldap/ldap-settings/ldap-settings.html | 35 ++ .../ldap/ldap-user-search-item/index.js | 14 + .../ldap-user-search-item.controller.js | 67 +++ .../ldap-user-search-item.html | 75 ++++ .../ldap/ldap-user-search/index.js | 14 + .../ldap-user-search.controller.js | 38 ++ .../ldap-user-search/ldap-user-search.html | 33 ++ .../ldap/ldap-users-datatable/index.js | 12 + .../ldap-users-datatable.html | 71 +++ .../settings/authentication/ldap/ldap.rest.js | 15 + .../authentication/ldap/ldap.service.js | 29 ++ app/portainer/settings/index.js | 3 +- .../settingsAuthentication.html | 399 ++--------------- .../settingsAuthenticationController.js | 405 ++++++++++-------- 61 files changed, 2329 insertions(+), 546 deletions(-) create mode 100644 app/portainer/settings/authentication/auth-method-constants.js create mode 100644 app/portainer/settings/authentication/auth-type-constants.js create mode 100644 app/portainer/settings/authentication/auto-team-membership-toggle/auto-team-membership-toggle.html create mode 100644 app/portainer/settings/authentication/auto-team-membership-toggle/index.js create mode 100644 app/portainer/settings/authentication/auto-user-provision-toggle/auto-user-provision-toggle.html create mode 100644 app/portainer/settings/authentication/auto-user-provision-toggle/index.js create mode 100644 app/portainer/settings/authentication/index.js create mode 100644 app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ad-settings/ad-settings.html create mode 100644 app/portainer/settings/authentication/ldap/ad-settings/index.js create mode 100644 app/portainer/settings/authentication/ldap/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-connectivity-check/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-connectivity-check/ldap-connectivity-check.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-custom-group-search/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-custom-user-search/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-group-search-item/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-group-search/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-groups-datatable/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-groups-datatable/ldap-groups-datatable.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-custom/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-openldap/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-security/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-security/ldap-settings-security.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-test-login/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings.model.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-user-search-item/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-user-search/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.controller.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.html create mode 100644 app/portainer/settings/authentication/ldap/ldap-users-datatable/index.js create mode 100644 app/portainer/settings/authentication/ldap/ldap-users-datatable/ldap-users-datatable.html create mode 100644 app/portainer/settings/authentication/ldap/ldap.rest.js create mode 100644 app/portainer/settings/authentication/ldap/ldap.service.js diff --git a/app/portainer/settings/authentication/auth-method-constants.js b/app/portainer/settings/authentication/auth-method-constants.js new file mode 100644 index 000000000..dc9bf0a0c --- /dev/null +++ b/app/portainer/settings/authentication/auth-method-constants.js @@ -0,0 +1,11 @@ +export const authenticationMethodTypesMap = { + INTERNAL: 1, + LDAP: 2, + OAUTH: 3, +}; + +export const authenticationMethodTypesLabels = { + [authenticationMethodTypesMap.INTERNAL]: 'Internal', + [authenticationMethodTypesMap.LDAP]: 'LDAP', + [authenticationMethodTypesMap.OAUTH]: 'OAuth', +}; diff --git a/app/portainer/settings/authentication/auth-type-constants.js b/app/portainer/settings/authentication/auth-type-constants.js new file mode 100644 index 000000000..84de1d959 --- /dev/null +++ b/app/portainer/settings/authentication/auth-type-constants.js @@ -0,0 +1,11 @@ +export const authenticationActivityTypesMap = { + AuthSuccess: 1, + AuthFailure: 2, + Logout: 3, +}; + +export const authenticationActivityTypesLabels = { + [authenticationActivityTypesMap.AuthSuccess]: 'Authentication success', + [authenticationActivityTypesMap.AuthFailure]: 'Authentication failure', + [authenticationActivityTypesMap.Logout]: 'Logout', +}; diff --git a/app/portainer/settings/authentication/auto-team-membership-toggle/auto-team-membership-toggle.html b/app/portainer/settings/authentication/auto-team-membership-toggle/auto-team-membership-toggle.html new file mode 100644 index 000000000..19a9d9ab6 --- /dev/null +++ b/app/portainer/settings/authentication/auto-team-membership-toggle/auto-team-membership-toggle.html @@ -0,0 +1,14 @@ +
+ Team membership +
+
+ +
+
+
+ + +
+
diff --git a/app/portainer/settings/authentication/auto-team-membership-toggle/index.js b/app/portainer/settings/authentication/auto-team-membership-toggle/index.js new file mode 100644 index 000000000..6ad9c60c1 --- /dev/null +++ b/app/portainer/settings/authentication/auto-team-membership-toggle/index.js @@ -0,0 +1,9 @@ +export const autoTeamMembershipToggle = { + templateUrl: './auto-team-membership-toggle.html', + transclude: { + description: 'fieldDescription', + }, + bindings: { + ngModel: '=', + }, +}; diff --git a/app/portainer/settings/authentication/auto-user-provision-toggle/auto-user-provision-toggle.html b/app/portainer/settings/authentication/auto-user-provision-toggle/auto-user-provision-toggle.html new file mode 100644 index 000000000..aac43263a --- /dev/null +++ b/app/portainer/settings/authentication/auto-user-provision-toggle/auto-user-provision-toggle.html @@ -0,0 +1,14 @@ +
+ Automatic user provisioning +
+
+ +
+
+
+ + +
+
diff --git a/app/portainer/settings/authentication/auto-user-provision-toggle/index.js b/app/portainer/settings/authentication/auto-user-provision-toggle/index.js new file mode 100644 index 000000000..68c7b95d1 --- /dev/null +++ b/app/portainer/settings/authentication/auto-user-provision-toggle/index.js @@ -0,0 +1,9 @@ +export const autoUserProvisionToggle = { + templateUrl: './auto-user-provision-toggle.html', + transclude: { + description: 'fieldDescription', + }, + bindings: { + ngModel: '=', + }, +}; diff --git a/app/portainer/settings/authentication/index.js b/app/portainer/settings/authentication/index.js new file mode 100644 index 000000000..a6e907ebe --- /dev/null +++ b/app/portainer/settings/authentication/index.js @@ -0,0 +1,12 @@ +import angular from 'angular'; + +import ldapModule from './ldap'; + +import { autoUserProvisionToggle } from './auto-user-provision-toggle'; +import { autoTeamMembershipToggle } from './auto-team-membership-toggle'; + +export default angular + .module('portainer.settings.authentication', [ldapModule]) + + .component('autoUserProvisionToggle', autoUserProvisionToggle) + .component('autoTeamMembershipToggle', autoTeamMembershipToggle).name; diff --git a/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js b/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js new file mode 100644 index 000000000..4a988b321 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js @@ -0,0 +1,61 @@ +import _ from 'lodash-es'; + +export default class AdSettingsController { + /* @ngInject */ + constructor(LDAPService) { + this.LDAPService = LDAPService; + + this.domainSuffix = ''; + + this.onTlscaCertChange = this.onTlscaCertChange.bind(this); + this.searchUsers = this.searchUsers.bind(this); + this.searchGroups = this.searchGroups.bind(this); + this.parseDomainName = this.parseDomainName.bind(this); + this.onAccountChange = this.onAccountChange.bind(this); + } + + parseDomainName(account) { + this.domainName = ''; + + if (!account || !account.includes('@')) { + return; + } + + const [, domainName] = account.split('@'); + if (!domainName) { + return; + } + + const parts = _.compact(domainName.split('.')); + this.domainSuffix = parts.map((part) => `dc=${part}`).join(','); + } + + onAccountChange(account) { + this.parseDomainName(account); + } + + searchUsers() { + return this.LDAPService.users(this.settings); + } + + searchGroups() { + return this.LDAPService.groups(this.settings); + } + + onTlscaCertChange(file) { + this.tlscaCert = file; + } + + addLDAPUrl() { + this.settings.URLs.push(''); + } + + removeLDAPUrl(index) { + this.settings.URLs.splice(index, 1); + } + + $onInit() { + this.tlscaCert = this.settings.TLSCACert; + this.parseDomainName(this.settings.ReaderDN); + } +} diff --git a/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.html b/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.html new file mode 100644 index 000000000..29c7d7008 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.html @@ -0,0 +1,115 @@ + + + With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s). If + disabled, users must be created in Portainer beforehand. + + + +
+
+ Information +
+
+ When using Microsoft AD authentication, Portainer will delegate user authentication to the Domain Controller(s) configured below; if there is no connectivity, Portainer will + fallback to internal authentication. +
+
+ +
+ AD configuration +
+ +
+
+

+ + You can configure multiple AD Controllers for authentication fallback. Make sure all servers are using the same configuration (i.e. if TLS is enabled, they should all use the + same certificates). +

+
+
+ +
+ +
+
+ + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + + + + + + + + + + + diff --git a/app/portainer/settings/authentication/ldap/ad-settings/index.js b/app/portainer/settings/authentication/ldap/ad-settings/index.js new file mode 100644 index 000000000..59a474097 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ad-settings/index.js @@ -0,0 +1,12 @@ +import controller from './ad-settings.controller'; + +export const adSettings = { + templateUrl: './ad-settings.html', + controller, + bindings: { + settings: '=', + tlscaCert: '=', + state: '=', + connectivityCheck: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/index.js b/app/portainer/settings/authentication/ldap/index.js new file mode 100644 index 000000000..2b3612be8 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/index.js @@ -0,0 +1,44 @@ +import angular from 'angular'; + +import { adSettings } from './ad-settings'; +import { ldapSettings } from './ldap-settings'; +import { ldapSettingsCustom } from './ldap-settings-custom'; +import { ldapSettingsOpenLdap } from './ldap-settings-openldap'; + +import { ldapConnectivityCheck } from './ldap-connectivity-check'; +import { ldapGroupsDatatable } from './ldap-groups-datatable'; +import { ldapGroupSearch } from './ldap-group-search'; +import { ldapGroupSearchItem } from './ldap-group-search-item'; +import { ldapUserSearch } from './ldap-user-search'; +import { ldapUserSearchItem } from './ldap-user-search-item'; +import { ldapSettingsDnBuilder } from './ldap-settings-dn-builder'; +import { ldapSettingsGroupDnBuilder } from './ldap-settings-group-dn-builder'; +import { ldapCustomGroupSearch } from './ldap-custom-group-search'; +import { ldapSettingsSecurity } from './ldap-settings-security'; +import { ldapSettingsTestLogin } from './ldap-settings-test-login'; +import { ldapCustomUserSearch } from './ldap-custom-user-search'; +import { ldapUsersDatatable } from './ldap-users-datatable'; +import { LDAPService } from './ldap.service'; +import { LDAP } from './ldap.rest'; + +export default angular + .module('portainer.settings.authentication.ldap', []) + .service('LDAPService', LDAPService) + .service('LDAP', LDAP) + .component('ldapConnectivityCheck', ldapConnectivityCheck) + .component('ldapGroupsDatatable', ldapGroupsDatatable) + .component('ldapSettings', ldapSettings) + .component('adSettings', adSettings) + .component('ldapGroupSearch', ldapGroupSearch) + .component('ldapGroupSearchItem', ldapGroupSearchItem) + .component('ldapUserSearch', ldapUserSearch) + .component('ldapUserSearchItem', ldapUserSearchItem) + .component('ldapSettingsCustom', ldapSettingsCustom) + .component('ldapSettingsDnBuilder', ldapSettingsDnBuilder) + .component('ldapSettingsGroupDnBuilder', ldapSettingsGroupDnBuilder) + .component('ldapCustomGroupSearch', ldapCustomGroupSearch) + .component('ldapSettingsOpenLdap', ldapSettingsOpenLdap) + .component('ldapSettingsSecurity', ldapSettingsSecurity) + .component('ldapSettingsTestLogin', ldapSettingsTestLogin) + .component('ldapCustomUserSearch', ldapCustomUserSearch) + .component('ldapUsersDatatable', ldapUsersDatatable).name; diff --git a/app/portainer/settings/authentication/ldap/ldap-connectivity-check/index.js b/app/portainer/settings/authentication/ldap/ldap-connectivity-check/index.js new file mode 100644 index 000000000..b8a7fd136 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-connectivity-check/index.js @@ -0,0 +1,8 @@ +export const ldapConnectivityCheck = { + templateUrl: './ldap-connectivity-check.html', + bindings: { + settings: '<', + state: '<', + connectivityCheck: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-connectivity-check/ldap-connectivity-check.html b/app/portainer/settings/authentication/ldap/ldap-connectivity-check/ldap-connectivity-check.html new file mode 100644 index 000000000..f69a5a699 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-connectivity-check/ldap-connectivity-check.html @@ -0,0 +1,19 @@ +
+ +
+ +
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-custom-group-search/index.js b/app/portainer/settings/authentication/ldap/ldap-custom-group-search/index.js new file mode 100644 index 000000000..5f6c10ce9 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-custom-group-search/index.js @@ -0,0 +1,10 @@ +import controller from './ldap-custom-group-search.controller'; + +export const ldapCustomGroupSearch = { + templateUrl: './ldap-custom-group-search.html', + controller, + bindings: { + settings: '=', + onSearchClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.controller.js b/app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.controller.js new file mode 100644 index 000000000..4c746f50a --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.controller.js @@ -0,0 +1,34 @@ +export default class LdapCustomGroupSearchController { + /* @ngInject */ + constructor($async, Notifications) { + Object.assign(this, { $async, Notifications }); + + this.groups = null; + this.showTable = false; + + this.onRemoveClick = this.onRemoveClick.bind(this); + this.onAddClick = this.onAddClick.bind(this); + this.search = this.search.bind(this); + } + + onAddClick() { + this.settings.push({ GroupBaseDN: '', GroupAttribute: '', GroupFilter: '' }); + } + + onRemoveClick(index) { + this.settings.splice(index, 1); + } + + search() { + return this.$async(async () => { + try { + this.groups = null; + this.showTable = true; + this.groups = await this.onSearchClick(); + } catch (error) { + this.showTable = false; + this.Notifications.error('Failure', error, 'Failed to search users'); + } + }); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.html b/app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.html new file mode 100644 index 000000000..2423aa316 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-custom-group-search/ldap-custom-group-search.html @@ -0,0 +1,64 @@ +
+ Teams auto-population configurations +
+ + + +
+ + Extra search configuration + +
+ +
+ +
+ +
+ + +
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-custom-user-search/index.js b/app/portainer/settings/authentication/ldap/ldap-custom-user-search/index.js new file mode 100644 index 000000000..9163676e6 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-custom-user-search/index.js @@ -0,0 +1,10 @@ +import controller from './ldap-custom-user-search.controller'; + +export const ldapCustomUserSearch = { + templateUrl: './ldap-custom-user-search.html', + controller, + bindings: { + settings: '=', + onSearchClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.controller.js b/app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.controller.js new file mode 100644 index 000000000..e672e9ed4 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.controller.js @@ -0,0 +1,33 @@ +export default class LdapCustomUserSearchController { + /* @ngInject */ + constructor($async, Notifications) { + Object.assign(this, { $async, Notifications }); + + this.users = null; + + this.onRemoveClick = this.onRemoveClick.bind(this); + this.onAddClick = this.onAddClick.bind(this); + this.search = this.search.bind(this); + } + + onAddClick() { + this.settings.push({ BaseDN: '', UserNameAttribute: '', Filter: '' }); + } + + onRemoveClick(index) { + this.settings.splice(index, 1); + } + + search() { + return this.$async(async () => { + try { + this.users = null; + this.showTable = true; + this.users = await this.onSearchClick(); + } catch (error) { + this.showTable = false; + this.Notifications.error('Failure', error, 'Failed to search users'); + } + }); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.html b/app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.html new file mode 100644 index 000000000..df7c3114b --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-custom-user-search/ldap-custom-user-search.html @@ -0,0 +1,64 @@ +
+ User search configurations +
+ + + +
+ + Extra search configuration + +
+ +
+ +
+ +
+ + +
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-group-search-item/index.js b/app/portainer/settings/authentication/ldap/ldap-group-search-item/index.js new file mode 100644 index 000000000..929db04a6 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-group-search-item/index.js @@ -0,0 +1,14 @@ +import controller from './ldap-group-search-item.controller'; + +export const ldapGroupSearchItem = { + templateUrl: './ldap-group-search-item.html', + controller, + bindings: { + config: '=', + index: '<', + domainSuffix: '@', + baseFilter: '@', + + onRemoveClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.controller.js b/app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.controller.js new file mode 100644 index 000000000..95a1cc31a --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.controller.js @@ -0,0 +1,51 @@ +export default class LdapSettingsAdGroupSearchItemController { + /* @ngInject */ + constructor(Notifications) { + Object.assign(this, { Notifications }); + + this.groups = []; + + this.onChangeBaseDN = this.onChangeBaseDN.bind(this); + } + + onChangeBaseDN(baseDN) { + this.config.GroupBaseDN = baseDN; + } + + addGroup() { + this.groups.push({ type: 'ou', value: '' }); + } + + removeGroup($index) { + this.groups.splice($index, 1); + this.onGroupsChange(); + } + + onGroupsChange() { + const groupsFilter = this.groups.map(({ type, value }) => `(${type}=${value})`).join(''); + this.onFilterChange(groupsFilter ? `(&${this.baseFilter}(|${groupsFilter}))` : `${this.baseFilter}`); + } + + onFilterChange(filter) { + this.config.GroupFilter = filter; + } + + parseGroupFilter() { + const match = this.config.GroupFilter.match(/^\(&\(objectClass=(\w+)\)\(\|((\(\w+=.+\))+)\)\)$/); + if (!match) { + return; + } + + const [, , groupFilter = ''] = match; + + this.groups = groupFilter + .slice(1, -1) + .split(')(') + .map((str) => str.split('=')) + .map(([type, value]) => ({ type, value })); + } + + $onInit() { + this.parseGroupFilter(); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.html b/app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.html new file mode 100644 index 000000000..fa5a86680 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-group-search-item/ldap-group-search-item.html @@ -0,0 +1,68 @@ + + +
+ + Extra search configuration + + +
+ + + +
+ +
+ {{ $ctrl.config.GroupBaseDN }} +
+
+ +
+
+ + + add another group + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+ +
+ {{ $ctrl.config.GroupFilter }} +
+
+
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-group-search/index.js b/app/portainer/settings/authentication/ldap/ldap-group-search/index.js new file mode 100644 index 000000000..99bb6f061 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-group-search/index.js @@ -0,0 +1,13 @@ +import controller from './ldap-group-search.controller'; + +export const ldapGroupSearch = { + templateUrl: './ldap-group-search.html', + controller, + bindings: { + settings: '=', + domainSuffix: '@', + baseFilter: '@', + + onSearchClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.controller.js b/app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.controller.js new file mode 100644 index 000000000..c431bb230 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.controller.js @@ -0,0 +1,36 @@ +import _ from 'lodash-es'; + +export default class LdapGroupSearchController { + /* @ngInject */ + constructor($async, Notifications) { + Object.assign(this, { $async, Notifications }); + + this.groups = null; + + this.onRemoveClick = this.onRemoveClick.bind(this); + this.onAddClick = this.onAddClick.bind(this); + this.search = this.search.bind(this); + } + + onAddClick() { + const lastSetting = _.last(this.settings); + this.settings.push({ GroupBaseDN: this.domainSuffix, GroupAttribute: lastSetting.GroupAttribute, GroupFilter: this.baseFilter }); + } + + onRemoveClick(index) { + this.settings.splice(index, 1); + } + + search() { + return this.$async(async () => { + try { + this.groups = null; + this.showTable = true; + this.groups = await this.onSearchClick(); + } catch (error) { + this.showTable = false; + this.Notifications.error('Failure', error, 'Failed to search users'); + } + }); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.html b/app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.html new file mode 100644 index 000000000..1b5840f6d --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-group-search/ldap-group-search.html @@ -0,0 +1,32 @@ +
+ Teams auto-population configurations +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-groups-datatable/index.js b/app/portainer/settings/authentication/ldap/ldap-groups-datatable/index.js new file mode 100644 index 000000000..28cacef0c --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-groups-datatable/index.js @@ -0,0 +1,12 @@ +export const ldapGroupsDatatable = { + templateUrl: './ldap-groups-datatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-groups-datatable/ldap-groups-datatable.html b/app/portainer/settings/authentication/ldap/ldap-groups-datatable/ldap-groups-datatable.html new file mode 100644 index 000000000..061448f70 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-groups-datatable/ldap-groups-datatable.html @@ -0,0 +1,77 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ + User Name + + + + + Groups +
+ {{ item.Name }} + +

{{ group }}

+
Loading...
No groups found.
+
+ +
+
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-custom/index.js b/app/portainer/settings/authentication/ldap/ldap-settings-custom/index.js new file mode 100644 index 000000000..321223717 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-custom/index.js @@ -0,0 +1,15 @@ +import controller from './ldap-settings-custom.controller'; + +export const ldapSettingsCustom = { + templateUrl: './ldap-settings-custom.html', + controller, + bindings: { + settings: '=', + tlscaCert: '=', + state: '=', + onTlscaCertChange: '<', + connectivityCheck: '<', + onSearchUsersClick: '<', + onSearchGroupsClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js new file mode 100644 index 000000000..6fbea91fb --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js @@ -0,0 +1,9 @@ +export default class LdapSettingsCustomController { + addLDAPUrl() { + this.settings.URLs.push(''); + } + + removeLDAPUrl(index) { + this.settings.URLs.splice(index, 1); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.html b/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.html new file mode 100644 index 000000000..14cc35279 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.html @@ -0,0 +1,99 @@ +
+
+ Information +
+
+ When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails. +
+
+ +
+ LDAP configuration +
+ +
+
+

+ + You can configure multiple LDAP Servers for authentication fallback. Make sure all servers are using the same configuration (i.e. if TLS is enabled, they should all use the + same certificates). +

+
+
+ +
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + + + + + + + + + + diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/index.js b/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/index.js new file mode 100644 index 000000000..bedcd03c8 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/index.js @@ -0,0 +1,15 @@ +import controller from './ldap-settings-dn-builder.controller'; + +export const ldapSettingsDnBuilder = { + templateUrl: './ldap-settings-dn-builder.html', + controller, + bindings: { + // ngModel: string (dc=,cn=,) + ngModel: '<', + // onChange(string) => void + onChange: '<', + // suffix: string (dc=,dc=,) + suffix: '@', + label: '@', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.controller.js new file mode 100644 index 000000000..4b829967a --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.controller.js @@ -0,0 +1,84 @@ +export default class LdapSettingsBaseDnBuilderController { + /* @ngInject */ + constructor() { + this.entries = []; + } + + addEntry() { + this.entries.push({ type: 'ou', value: '' }); + } + + removeEntry($index) { + this.entries.splice($index, 1); + this.onEntriesChange(); + } + + moveUp($index) { + if ($index <= 0) { + return; + } + arrayMove(this.entries, $index, $index - 1); + this.onEntriesChange(); + } + + moveDown($index) { + if ($index >= this.entries.length - 1) { + return; + } + arrayMove(this.entries, $index, $index + 1); + this.onEntriesChange(); + } + + onEntriesChange() { + const dn = this.entries + .filter(({ value }) => value) + .map(({ type, value }) => `${type}=${value}`) + .concat(this.suffix) + .filter((value) => value) + .join(','); + + this.onChange(dn); + } + + getOUValues(dn, domainSuffix = '') { + const regex = /(\w+)=(\w*),?/; + let ouValues = []; + let left = dn; + let match = left.match(regex); + while (match && left !== domainSuffix) { + const [, type, value] = match; + ouValues.push({ type, value }); + left = left.replace(regex, ''); + match = left.match(/(\w+)=(\w+),?/); + } + return ouValues; + } + + parseBaseDN() { + this.entries = this.getOUValues(this.ngModel, this.suffix); + } + + $onChanges({ suffix, ngModel }) { + if ((!suffix && !ngModel) || (suffix && suffix.isFirstChange())) { + return; + } + this.onEntriesChange(); + } + + $onInit() { + this.parseBaseDN(); + } +} + +function arrayMove(array, fromIndex, toIndex) { + if (!checkValidIndex(array, fromIndex) || !checkValidIndex(array, toIndex)) { + throw new Error('index is out of bounds'); + } + const [item] = array.splice(fromIndex, 1); + + array.splice(toIndex, 0, item); + + function checkValidIndex(array, index) { + return index >= 0 && index <= array.length; + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.html b/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.html new file mode 100644 index 000000000..4ce6c1bdc --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-dn-builder/ldap-settings-dn-builder.html @@ -0,0 +1,36 @@ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ + + +
+
+
+
+
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/index.js b/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/index.js new file mode 100644 index 000000000..e3d0818cd --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/index.js @@ -0,0 +1,17 @@ +import controller from './ldap-settings-group-dn-builder.controller'; + +export const ldapSettingsGroupDnBuilder = { + templateUrl: './ldap-settings-group-dn-builder.html', + controller, + bindings: { + // ngModel: string (dc=,cn=,) + ngModel: '<', + // onChange(string) => void + onChange: '<', + // suffix: string (dc=,dc=,) + suffix: '@', + // index: int >= 0 + index: '<', + onRemoveClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.controller.js new file mode 100644 index 000000000..32ee7f3ee --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.controller.js @@ -0,0 +1,55 @@ +export default class LdapSettingsGroupDnBuilderController { + /* @ngInject */ + constructor() { + this.groupName = ''; + this.entries = ''; + + this.onEntriesChange = this.onEntriesChange.bind(this); + this.onGroupNameChange = this.onGroupNameChange.bind(this); + this.onGroupChange = this.onGroupChange.bind(this); + this.removeGroup = this.removeGroup.bind(this); + } + + onEntriesChange(entries) { + this.onGroupChange(this.groupName, entries); + } + + onGroupNameChange() { + this.onGroupChange(this.groupName, this.entries); + } + + onGroupChange(groupName, entries) { + if (!groupName) { + return; + } + const groupNameEntry = `cn=${groupName}`; + this.onChange(this.index, entries || this.suffix ? `${groupNameEntry},${entries || this.suffix}` : groupNameEntry); + } + + removeGroup() { + this.onRemoveClick(this.index); + } + + parseEntries(value, suffix) { + if (value === suffix) { + this.groupName = ''; + this.entries = suffix; + return; + } + + const [groupName, entries] = this.ngModel.split(/,(.+)/); + this.groupName = groupName.replace('cn=', ''); + this.entries = entries || ''; + } + + $onChange({ ngModel, suffix }) { + if ((!suffix || suffix.isFirstChange()) && !ngModel) { + return; + } + this.parseEntries(ngModel.value, suffix.value); + } + + $onInit() { + this.parseEntries(this.ngModel, this.suffix); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.html b/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.html new file mode 100644 index 000000000..8f138ff1b --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-group-dn-builder/ldap-settings-group-dn-builder.html @@ -0,0 +1,21 @@ +
+ +
+ +
+
+ +
+
+ + diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-openldap/index.js b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/index.js new file mode 100644 index 000000000..b88f8008c --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/index.js @@ -0,0 +1,16 @@ +import controller from './ldap-settings-openldap.controller'; + +export const ldapSettingsOpenLdap = { + templateUrl: './ldap-settings-openldap.html', + controller, + bindings: { + settings: '=', + tlscaCert: '=', + state: '=', + connectivityCheck: '<', + + onTlscaCertChange: '<', + onSearchUsersClick: '<', + onSearchGroupsClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js new file mode 100644 index 000000000..fd37e4ff7 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js @@ -0,0 +1,42 @@ +export default class LdapSettingsOpenLDAPController { + /* @ngInject */ + constructor() { + this.domainSuffix = ''; + + this.findDomainSuffix = this.findDomainSuffix.bind(this); + this.parseDomainSuffix = this.parseDomainSuffix.bind(this); + this.onAccountChange = this.onAccountChange.bind(this); + } + + findDomainSuffix() { + const serviceAccount = this.settings.ReaderDN; + let domainSuffix = this.parseDomainSuffix(serviceAccount); + if (!domainSuffix && this.settings.SearchSettings.length > 0) { + const searchSettings = this.settings.SearchSettings[0]; + domainSuffix = this.parseDomainSuffix(searchSettings.BaseDN); + } + + this.domainSuffix = domainSuffix; + } + + parseDomainSuffix(string = '') { + const index = string.toLowerCase().indexOf('dc='); + return index !== -1 ? string.substring(index) : ''; + } + + onAccountChange(serviceAccount) { + this.domainSuffix = this.parseDomainSuffix(serviceAccount); + } + + addLDAPUrl() { + this.settings.URLs.push(''); + } + + removeLDAPUrl(index) { + this.settings.URLs.splice(index, 1); + } + + $onInit() { + this.findDomainSuffix(); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.html b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.html new file mode 100644 index 000000000..3b84f5f44 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.html @@ -0,0 +1,129 @@ +
+
+ Information +
+
+ When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails. +
+
+ +
+ LDAP configuration +
+ +
+
+

+ + You can configure multiple LDAP Servers for authentication fallback. Make sure all servers are using the same configuration (i.e. if TLS is enabled, they should all use the + same certificates). +

+
+
+ +
+ +
+
+ + +
+
+
+ + +
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+
+ + + + + + + + + + + + diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-security/index.js b/app/portainer/settings/authentication/ldap/ldap-settings-security/index.js new file mode 100644 index 000000000..8a4c43ac4 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-security/index.js @@ -0,0 +1,10 @@ +export const ldapSettingsSecurity = { + templateUrl: './ldap-settings-security.html', + bindings: { + settings: '=', + tlscaCert: '<', + onTlscaCertChange: '<', + uploadInProgress: '<', + title: '@', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-security/ldap-settings-security.html b/app/portainer/settings/authentication/ldap/ldap-settings-security/ldap-settings-security.html new file mode 100644 index 000000000..527c8fcc2 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-security/ldap-settings-security.html @@ -0,0 +1,57 @@ +
+ {{ $ctrl.title || 'LDAP security' }} +
+ + +
+ +
+ +
+
+ + + +
+ +
+ +
+
+ + + +
+ +
+ +
+
+ + + +
+ +
+ + + {{ $ctrl.tlscaCert.name }} + + + + +
+
+ diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-test-login/index.js b/app/portainer/settings/authentication/ldap/ldap-settings-test-login/index.js new file mode 100644 index 000000000..e4aefc097 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-test-login/index.js @@ -0,0 +1,9 @@ +import controller from './ldap-settings-test-login.controller'; + +export const ldapSettingsTestLogin = { + templateUrl: './ldap-settings-test-login.html', + controller, + bindings: { + settings: '=', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.controller.js new file mode 100644 index 000000000..811f70aa9 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.controller.js @@ -0,0 +1,31 @@ +const TEST_STATUS = { + LOADING: 'LOADING', + SUCCESS: 'SUCCESS', + FAILURE: 'FAILURE', +}; + +export default class LdapSettingsTestLogin { + /* @ngInject */ + constructor($async, LDAPService, Notifications) { + Object.assign(this, { $async, LDAPService, Notifications }); + + this.TEST_STATUS = TEST_STATUS; + + this.state = { + testStatus: '', + }; + } + + async testLogin(username, password) { + return this.$async(async () => { + this.state.testStatus = TEST_STATUS.LOADING; + try { + const response = await this.LDAPService.testLogin(this.settings, username, password); + this.state.testStatus = response.valid ? TEST_STATUS.SUCCESS : TEST_STATUS.FAILURE; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to test login'); + this.state.testStatus = TEST_STATUS.FAILURE; + } + }); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.html b/app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.html new file mode 100644 index 000000000..a69b89674 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings-test-login/ldap-settings-test-login.html @@ -0,0 +1,32 @@ +
+ Test login +
+
+
+ + +
+ +
+ + +
+ +
+ + + +
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-settings.model.js b/app/portainer/settings/authentication/ldap/ldap-settings.model.js new file mode 100644 index 000000000..d14711eec --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings.model.js @@ -0,0 +1,54 @@ +export function buildLdapSettingsModel() { + return { + AnonymousMode: true, + ReaderDN: '', + URLs: [''], + ServerType: 0, + TLSConfig: { + TLS: false, + TLSSkipVerify: false, + }, + StartTLS: false, + SearchSettings: [ + { + BaseDN: '', + Filter: '', + UserNameAttribute: '', + }, + ], + GroupSearchSettings: [ + { + GroupBaseDN: '', + GroupFilter: '', + GroupAttribute: '', + }, + ], + AutoCreateUsers: true, + }; +} + +export function buildAdSettingsModel() { + const settings = buildLdapSettingsModel(); + + settings.ServerType = 2; + settings.AnonymousMode = false; + settings.SearchSettings[0].UserNameAttribute = 'sAMAccountName'; + settings.SearchSettings[0].Filter = '(objectClass=user)'; + settings.GroupSearchSettings[0].GroupAttribute = 'member'; + settings.GroupSearchSettings[0].GroupFilter = '(objectClass=group)'; + + return settings; +} + +export function buildOpenLDAPSettingsModel() { + const settings = buildLdapSettingsModel(); + + settings.ServerType = 1; + settings.AnonymousMode = false; + settings.SearchSettings[0].UserNameAttribute = 'uid'; + settings.SearchSettings[0].Filter = '(objectClass=inetOrgPerson)'; + settings.GroupSearchSettings[0].GroupAttribute = 'member'; + settings.GroupSearchSettings[0].GroupFilter = '(objectClass=groupOfNames)'; + + return settings; +} diff --git a/app/portainer/settings/authentication/ldap/ldap-settings/index.js b/app/portainer/settings/authentication/ldap/ldap-settings/index.js new file mode 100644 index 000000000..90e86951e --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings/index.js @@ -0,0 +1,11 @@ +import controller from './ldap-settings.controller'; + +export const ldapSettings = { + templateUrl: './ldap-settings.html', + controller, + bindings: { + settings: '=', + state: '<', + connectivityCheck: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js new file mode 100644 index 000000000..306740e18 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js @@ -0,0 +1,66 @@ +const SERVER_TYPES = { + CUSTOM: 0, + OPEN_LDAP: 1, + AD: 2, +}; + +import { buildOpenLDAPSettingsModel } from '@/portainer/settings/authentication/ldap/ldap-settings.model'; + +const DEFAULT_GROUP_FILTER = '(objectClass=groupOfNames)'; +const DEFAULT_USER_FILTER = '(objectClass=inetOrgPerson)'; + +export default class LdapSettingsController { + /* @ngInject */ + constructor(LDAPService) { + Object.assign(this, { LDAPService, SERVER_TYPES }); + + this.tlscaCert = null; + + this.boxSelectorOptions = [ + { id: 'ldap_custom', value: SERVER_TYPES.CUSTOM, label: 'Custom', icon: 'fa fa-server' }, + { id: 'ldap_openldap', value: SERVER_TYPES.OPEN_LDAP, label: 'OpenLDAP', icon: 'fa fa-server' }, + ]; + + this.onTlscaCertChange = this.onTlscaCertChange.bind(this); + this.searchUsers = this.searchUsers.bind(this); + this.searchGroups = this.searchGroups.bind(this); + this.onChangeServerType = this.onChangeServerType.bind(this); + } + + onTlscaCertChange(file) { + this.tlscaCert = file; + } + + $onInit() { + this.tlscaCert = this.settings.TLSCACert; + } + + onChangeServerType(serverType) { + switch (serverType) { + case SERVER_TYPES.OPEN_LDAP: + return this.onChangeToOpenLDAP(); + default: + break; + } + } + + onChangeToOpenLDAP() { + this.settings = buildOpenLDAPSettingsModel(); + } + + searchUsers() { + const settings = { + ...this.settings, + SearchSettings: this.settings.SearchSettings.map((search) => ({ ...search, Filter: search.Filter || DEFAULT_USER_FILTER })), + }; + return this.LDAPService.users(settings); + } + + searchGroups() { + const settings = { + ...this.settings, + GroupSearchSettings: this.settings.GroupSearchSettings.map((search) => ({ ...search, GroupFilter: search.GroupFilter || DEFAULT_GROUP_FILTER })), + }; + return this.LDAPService.groups(settings); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html new file mode 100644 index 000000000..d00a84ba5 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html @@ -0,0 +1,35 @@ +
+ + + With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s). + If disabled, users must be created in Portainer beforehand. + + + +
+ Server Type +
+ + + + + +
diff --git a/app/portainer/settings/authentication/ldap/ldap-user-search-item/index.js b/app/portainer/settings/authentication/ldap/ldap-user-search-item/index.js new file mode 100644 index 000000000..414883713 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-user-search-item/index.js @@ -0,0 +1,14 @@ +import controller from './ldap-user-search-item.controller'; + +export const ldapUserSearchItem = { + templateUrl: './ldap-user-search-item.html', + controller, + bindings: { + config: '=', + index: '<', + showUsernameFormat: '<', + domainSuffix: '@', + baseFilter: '@', + onRemoveClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.controller.js b/app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.controller.js new file mode 100644 index 000000000..a42ffdc72 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.controller.js @@ -0,0 +1,67 @@ +export default class LdapUserSearchItemController { + /* @ngInject */ + constructor() { + this.groups = []; + + this.onBaseDNChange = this.onBaseDNChange.bind(this); + this.onGroupChange = this.onGroupChange.bind(this); + this.onGroupsChange = this.onGroupsChange.bind(this); + this.removeGroup = this.removeGroup.bind(this); + } + + onBaseDNChange(baseDN) { + this.config.BaseDN = baseDN; + } + + onGroupChange(index, group) { + this.groups[index] = group; + this.onGroupsChange(this.groups); + } + + onGroupsChange(groups) { + this.config.Filter = this.generateUserFilter(groups); + } + + removeGroup(index) { + this.groups.splice(index, 1); + this.onGroupsChange(this.groups); + } + + addGroup() { + this.groups.push(this.domainSuffix ? `cn=,${this.domainSuffix}` : 'cn='); + } + + generateUserFilter(groups) { + const filteredGroups = groups.filter((group) => group !== this.domainSuffix); + + if (!filteredGroups.length) { + return this.baseFilter; + } + + const groupsFilter = filteredGroups.map((group) => `(memberOf=${group})`); + + return `(&${this.baseFilter}${groupsFilter.length > 1 ? `(|${groupsFilter.join('')})` : groupsFilter[0]})`; + } + + parseFilter() { + const filter = this.config.Filter; + if (filter === this.baseFilter) { + return; + } + + if (!filter.includes('|')) { + const index = filter.indexOf('memberOf='); + if (index > -1) { + this.groups = [filter.slice(index + 9, -2)]; + } + return; + } + + const members = filter.slice(filter.indexOf('|') + 2, -3); + this.groups = members.split(')(').map((member) => member.replace('memberOf=', '')); + } + + $onInit() { + this.parseFilter(); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.html b/app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.html new file mode 100644 index 000000000..f21386f3e --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-user-search-item/ldap-user-search-item.html @@ -0,0 +1,75 @@ + + +
+ + Extra search configuration + + +
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ +
+ +
+ {{ $ctrl.config.BaseDN }} +
+
+ + + +
+
+ + +
+
+
+ + + + + +
+
+
+ +
+ +
+ {{ $ctrl.config.Filter }} +
+
+
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-user-search/index.js b/app/portainer/settings/authentication/ldap/ldap-user-search/index.js new file mode 100644 index 000000000..bb3d3009a --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-user-search/index.js @@ -0,0 +1,14 @@ +import controller from './ldap-user-search.controller'; + +export const ldapUserSearch = { + templateUrl: './ldap-user-search.html', + controller, + bindings: { + settings: '=', + domainSuffix: '@', + showUsernameFormat: '<', + baseFilter: '@', + + onSearchClick: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.controller.js b/app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.controller.js new file mode 100644 index 000000000..6d5ff11eb --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.controller.js @@ -0,0 +1,38 @@ +import _ from 'lodash'; + +export default class LdapUserSearchController { + /* @ngInject */ + constructor($async, Notifications) { + Object.assign(this, { $async, Notifications }); + + this.users = null; + this.showTable = false; + + this.onRemoveClick = this.onRemoveClick.bind(this); + this.onAddClick = this.onAddClick.bind(this); + this.search = this.search.bind(this); + } + + onAddClick() { + const lastSetting = _.last(this.settings); + this.settings.push({ BaseDN: this.domainSuffix, UserNameAttribute: lastSetting.UserNameAttribute, Filter: this.baseFilter }); + } + + onRemoveClick(index) { + this.settings.splice(index, 1); + } + + search() { + return this.$async(async () => { + try { + this.users = null; + this.showTable = true; + const users = await this.onSearchClick(); + this.users = _.compact(users); + } catch (error) { + this.Notifications.error('Failure', error, 'Failed to search users'); + this.showTable = false; + } + }); + } +} diff --git a/app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.html b/app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.html new file mode 100644 index 000000000..3d491b62c --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-user-search/ldap-user-search.html @@ -0,0 +1,33 @@ +
+ User search configurations +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/app/portainer/settings/authentication/ldap/ldap-users-datatable/index.js b/app/portainer/settings/authentication/ldap/ldap-users-datatable/index.js new file mode 100644 index 000000000..4c80771d4 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-users-datatable/index.js @@ -0,0 +1,12 @@ +export const ldapUsersDatatable = { + templateUrl: './ldap-users-datatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + }, +}; diff --git a/app/portainer/settings/authentication/ldap/ldap-users-datatable/ldap-users-datatable.html b/app/portainer/settings/authentication/ldap/ldap-users-datatable/ldap-users-datatable.html new file mode 100644 index 000000000..9817654f2 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap-users-datatable/ldap-users-datatable.html @@ -0,0 +1,71 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ +
+ + + + + + + + + + + + + + + + + +
+ + Name + + + +
+ {{ item }} +
Loading...
No users found.
+
+ +
+
+
diff --git a/app/portainer/settings/authentication/ldap/ldap.rest.js b/app/portainer/settings/authentication/ldap/ldap.rest.js new file mode 100644 index 000000000..e93d5277c --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap.rest.js @@ -0,0 +1,15 @@ +const API_ENDPOINT_LDAP = 'api/ldap'; + +/* @ngInject */ +export function LDAP($resource) { + return $resource( + `${API_ENDPOINT_LDAP}/:action`, + {}, + { + check: { method: 'POST', params: { action: 'check' } }, + users: { method: 'POST', isArray: true, params: { action: 'users' } }, + groups: { method: 'POST', isArray: true, params: { action: 'groups' } }, + testLogin: { method: 'POST', params: { action: 'test' } }, + } + ); +} diff --git a/app/portainer/settings/authentication/ldap/ldap.service.js b/app/portainer/settings/authentication/ldap/ldap.service.js new file mode 100644 index 000000000..875f83a02 --- /dev/null +++ b/app/portainer/settings/authentication/ldap/ldap.service.js @@ -0,0 +1,29 @@ +/* @ngInject */ +export function LDAPService(LDAP) { + return { users, groups, check, testLogin }; + + function users(ldapSettings) { + return LDAP.users({ ldapSettings }).$promise; + } + + async function groups(ldapSettings) { + const userGroups = await LDAP.groups({ ldapSettings }).$promise; + return userGroups.map(({ Name, Groups }) => { + let name = Name; + if (Name.includes(',') && Name.includes('=')) { + const [cnName] = Name.split(','); + const split = cnName.split('='); + name = split[1]; + } + return { Groups, Name: name }; + }); + } + + function check(ldapSettings) { + return LDAP.check({ ldapSettings }).$promise; + } + + function testLogin(ldapSettings, username, password) { + return LDAP.testLogin({ ldapSettings, username, password }).$promise; + } +} diff --git a/app/portainer/settings/index.js b/app/portainer/settings/index.js index 42e4e25ac..629f9b5ed 100644 --- a/app/portainer/settings/index.js +++ b/app/portainer/settings/index.js @@ -1,5 +1,6 @@ import angular from 'angular'; +import authenticationModule from './authentication'; import generalModule from './general'; -export default angular.module('portainer.settings', [generalModule]).name; +export default angular.module('portainer.settings', [authenticationModule, generalModule]).name; diff --git a/app/portainer/views/settings/authentication/settingsAuthentication.html b/app/portainer/views/settings/authentication/settingsAuthentication.html index 2d36bcd76..481f5a290 100644 --- a/app/portainer/views/settings/authentication/settingsAuthentication.html +++ b/app/portainer/views/settings/authentication/settingsAuthentication.html @@ -34,43 +34,10 @@
Authentication method
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
+ + +
Information
@@ -79,345 +46,23 @@
-
-
-
- Information -
-
- When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails. -
-
+ -
- LDAP configuration -
+ -
- -
- -
-
- - -
-
- - -
-
- - -
-
- -
- -
-
- -
- -
- -
-
-
- -
- -
- -
-
- -
- LDAP security -
- - -
-
- - -
-
- - - -
-
- - -
-
- - - -
-
- - -
-
- - - -
- -
- -
- - - {{ formValues.TLSCACert.name }} - - - - -
-
- -
- - -
- -
- - -
-
- -
- Automatic user provisioning -
-
- - With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group - name(s). If disabled, users must be created in Portainer beforehand. - -
-
-
- - -
-
- -
- User search configurations -
- - -
-
- - Extra search configuration - -
- -
- -
- -
- - -
- -
-
-
- -
- -
-
- -
-
- -
- - add user search configuration - -
-
- - -
- Group search configurations -
- - -
-
- - Extra search configuration - -
- -
- -
- -
- - -
- -
-
-
- -
- -
-
- -
-
- -
- - add group search configuration - -
-
- -
- -
-
- Provider -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - +
@@ -425,7 +70,13 @@
- diff --git a/app/portainer/views/settings/authentication/settingsAuthenticationController.js b/app/portainer/views/settings/authentication/settingsAuthenticationController.js index f03b88f46..555bec411 100644 --- a/app/portainer/views/settings/authentication/settingsAuthenticationController.js +++ b/app/portainer/views/settings/authentication/settingsAuthenticationController.js @@ -1,181 +1,244 @@ -angular.module('portainer.app').controller('SettingsAuthenticationController', [ - '$q', - '$scope', - '$state', - 'Notifications', - 'SettingsService', - 'FileUploadService', - 'TeamService', - function ($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService) { - $scope.state = { - successfulConnectivityCheck: false, - failedConnectivityCheck: false, - uploadInProgress: false, - connectivityCheckInProgress: false, - actionInProgress: false, - availableUserSessionTimeoutOptions: [ - { - key: '1 hour', - value: '1h', - }, - { - key: '4 hours', - value: '4h', - }, - { - key: '8 hours', - value: '8h', - }, - { - key: '24 hours', - value: '24h', - }, - { key: '1 week', value: `${24 * 7}h` }, - { key: '1 month', value: `${24 * 30}h` }, - { key: '6 months', value: `${24 * 30 * 6}h` }, - { key: '1 year', value: `${24 * 30 * 12}h` }, - ], - }; +import angular from 'angular'; +import _ from 'lodash-es'; - $scope.formValues = { - UserSessionTimeout: $scope.state.availableUserSessionTimeoutOptions[0], - TLSCACert: '', - LDAPSettings: { - AnonymousMode: true, - ReaderDN: '', - URL: '', - TLSConfig: { - TLS: false, - TLSSkipVerify: false, - }, - StartTLS: false, - SearchSettings: [ - { - BaseDN: '', - Filter: '', - UserNameAttribute: '', - }, - ], - GroupSearchSettings: [ - { - GroupBaseDN: '', - GroupFilter: '', - GroupAttribute: '', - }, - ], - AutoCreateUsers: true, +import { buildLdapSettingsModel, buildAdSettingsModel } from '@/portainer/settings/authentication/ldap/ldap-settings.model'; + +angular.module('portainer.app').controller('SettingsAuthenticationController', SettingsAuthenticationController); + +function SettingsAuthenticationController($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService, LDAPService) { + $scope.state = { + uploadInProgress: false, + actionInProgress: false, + availableUserSessionTimeoutOptions: [ + { + key: '1 hour', + value: '1h', }, - }; + { + key: '4 hours', + value: '4h', + }, + { + key: '8 hours', + value: '8h', + }, + { + key: '24 hours', + value: '24h', + }, + { key: '1 week', value: `${24 * 7}h` }, + { key: '1 month', value: `${24 * 30}h` }, + { key: '6 months', value: `${24 * 30 * 6}h` }, + { key: '1 year', value: `${24 * 30 * 12}h` }, + ], + }; - $scope.isOauthEnabled = function isOauthEnabled() { - return $scope.settings && $scope.settings.AuthenticationMethod === 3; - }; + $scope.formValues = { + UserSessionTimeout: $scope.state.availableUserSessionTimeoutOptions[0], + TLSCACert: '', + ldap: { + serverType: 0, + adSettings: buildAdSettingsModel(), + ldapSettings: buildLdapSettingsModel(), + }, + }; - $scope.addSearchConfiguration = function () { - $scope.formValues.LDAPSettings.SearchSettings.push({ BaseDN: '', UserNameAttribute: '', Filter: '' }); - }; + $scope.authOptions = [ + { id: 'auth_internal', icon: 'fa fa-users', label: 'Internal', description: 'Internal authentication mechanism', value: 1 }, + { id: 'auth_ldap', icon: 'fa fa-users', label: 'LDAP', description: 'LDAP authentication', value: 2 }, + { id: 'auth_ad', icon: 'fab fa-microsoft', label: 'Microsoft Active Directory', description: 'AD authentication', value: 4 }, + { id: 'auth_oauth', icon: 'fa fa-users', label: 'OAuth', description: 'OAuth authentication', value: 3 }, + ]; - $scope.removeSearchConfiguration = function (index) { - $scope.formValues.LDAPSettings.SearchSettings.splice(index, 1); - }; - - $scope.addGroupSearchConfiguration = function () { - $scope.formValues.LDAPSettings.GroupSearchSettings.push({ GroupBaseDN: '', GroupAttribute: '', GroupFilter: '' }); - }; - - $scope.removeGroupSearchConfiguration = function (index) { - $scope.formValues.LDAPSettings.GroupSearchSettings.splice(index, 1); - }; - - $scope.LDAPConnectivityCheck = function () { - var settings = angular.copy($scope.settings); - var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null; - - if ($scope.formValues.LDAPSettings.AnonymousMode) { - settings.LDAPSettings['ReaderDN'] = ''; - settings.LDAPSettings['Password'] = ''; - } - - var uploadRequired = ($scope.formValues.LDAPSettings.TLSConfig.TLS || $scope.formValues.LDAPSettings.StartTLS) && !$scope.formValues.LDAPSettings.TLSConfig.TLSSkipVerify; - $scope.state.uploadInProgress = uploadRequired; - - $scope.state.connectivityCheckInProgress = true; - $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(TLSCAFile, null, null)) - .then(function success() { - addLDAPDefaultPort(settings, $scope.formValues.LDAPSettings.TLSConfig.TLS); - return SettingsService.checkLDAPConnectivity(settings); - }) - .then(function success() { - $scope.state.failedConnectivityCheck = false; - $scope.state.successfulConnectivityCheck = true; - Notifications.success('Connection to LDAP successful'); - }) - .catch(function error(err) { - $scope.state.failedConnectivityCheck = true; - $scope.state.successfulConnectivityCheck = false; - Notifications.error('Failure', err, 'Connection to LDAP failed'); - }) - .finally(function final() { - $scope.state.uploadInProgress = false; - $scope.state.connectivityCheckInProgress = false; - }); - }; - - $scope.saveSettings = function () { - var settings = angular.copy($scope.settings); - var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null; - - if ($scope.formValues.LDAPSettings.AnonymousMode) { - settings.LDAPSettings['ReaderDN'] = ''; - settings.LDAPSettings['Password'] = ''; - } - - var uploadRequired = ($scope.formValues.LDAPSettings.TLSConfig.TLS || $scope.formValues.LDAPSettings.StartTLS) && !$scope.formValues.LDAPSettings.TLSConfig.TLSSkipVerify; - $scope.state.uploadInProgress = uploadRequired; - - $scope.state.actionInProgress = true; - $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(TLSCAFile, null, null)) - .then(function success() { - addLDAPDefaultPort(settings, $scope.formValues.LDAPSettings.TLSConfig.TLS); - return SettingsService.update(settings); - }) - .then(function success() { - Notifications.success('Authentication settings updated'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update authentication settings'); - }) - .finally(function final() { - $scope.state.uploadInProgress = false; - $scope.state.actionInProgress = false; - }); - }; - - // Add default port if :port is not defined in URL - function addLDAPDefaultPort(settings, tlsEnabled) { - if (settings.LDAPSettings.URL.indexOf(':') === -1) { - settings.LDAPSettings.URL += tlsEnabled ? ':636' : ':389'; - } + $scope.onChangeAuthMethod = function onChangeAuthMethod(value) { + if (value === 4) { + $scope.settings.AuthenticationMethod = 2; + $scope.formValues.ldap.serverType = 2; + return; } - function initView() { - $q.all({ - settings: SettingsService.settings(), - teams: TeamService.teams(), + if (value === 2) { + $scope.settings.AuthenticationMethod = 2; + $scope.formValues.ldap.serverType = $scope.formValues.ldap.ldapSettings.ServerType; + return; + } + + $scope.settings.AuthenticationMethod = value; + }; + + $scope.authenticationMethodSelected = function authenticationMethodSelected(value) { + if (!$scope.settings) { + return false; + } + + if (value === 4) { + return $scope.settings.AuthenticationMethod === 2 && $scope.formValues.ldap.serverType === 2; + } + + if (value === 2) { + return $scope.settings.AuthenticationMethod === 2 && $scope.formValues.ldap.serverType !== 2; + } + + return $scope.settings.AuthenticationMethod === value; + }; + + $scope.isOauthEnabled = function isOauthEnabled() { + return $scope.settings && $scope.settings.AuthenticationMethod === 3; + }; + + $scope.LDAPConnectivityCheck = LDAPConnectivityCheck; + function LDAPConnectivityCheck() { + const settings = angular.copy($scope.settings); + + const { settings: ldapSettings, uploadRequired, tlscaFile } = prepareLDAPSettings(); + settings.LDAPSettings = ldapSettings; + $scope.state.uploadInProgress = uploadRequired; + + $scope.state.connectivityCheckInProgress = true; + + $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(tlscaFile, null, null)) + .then(function success() { + return LDAPService.check(settings.LDAPSettings); }) - .then(function success(data) { - var settings = data.settings; - $scope.teams = data.teams; - $scope.settings = settings; - $scope.formValues.LDAPSettings = settings.LDAPSettings; - $scope.OAuthSettings = settings.OAuthSettings; - $scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve application settings'); - }); + .then(function success() { + $scope.state.failedConnectivityCheck = false; + $scope.state.successfulConnectivityCheck = true; + Notifications.success('Connection to LDAP successful'); + }) + .catch(function error(err) { + $scope.state.failedConnectivityCheck = true; + $scope.state.successfulConnectivityCheck = false; + Notifications.error('Failure', err, 'Connection to LDAP failed'); + }) + .finally(function final() { + $scope.state.uploadInProgress = false; + $scope.state.connectivityCheckInProgress = false; + }); + } + + $scope.saveSettings = function () { + const settings = angular.copy($scope.settings); + + const { settings: ldapSettings, uploadRequired, tlscaFile } = prepareLDAPSettings(); + settings.LDAPSettings = ldapSettings; + $scope.state.uploadInProgress = uploadRequired; + + $scope.state.actionInProgress = true; + + $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(tlscaFile, null, null)) + .then(function success() { + return SettingsService.update(settings); + }) + .then(function success() { + Notifications.success('Authentication settings updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update authentication settings'); + }) + .finally(function final() { + $scope.state.uploadInProgress = false; + $scope.state.actionInProgress = false; + }); + }; + + function prepareLDAPSettings() { + const tlscaCert = $scope.formValues.TLSCACert; + + const tlscaFile = tlscaCert !== $scope.settings.LDAPSettings.TLSConfig.TLSCACert ? tlscaCert : null; + + const isADServer = $scope.formValues.ldap.serverType === 2; + + const settings = isADServer ? $scope.formValues.ldap.adSettings : $scope.formValues.ldap.ldapSettings; + + if (settings.AnonymousMode && !isADServer) { + settings.ReaderDN = ''; + settings.Password = ''; } - initView(); - }, -]); + if (isADServer) { + settings.AnonymousMode = false; + } + + settings.URLs = settings.URLs.map((url) => { + if (url.includes(':')) { + return url; + } + return url + (settings.TLSConfig.TLS ? ':636' : ':389'); + }); + + const uploadRequired = (settings.TLSConfig.TLS || settings.StartTLS) && !settings.TLSConfig.TLSSkipVerify; + + settings.URL = settings.URLs[0]; + + return { settings, uploadRequired, tlscaFile }; + } + + $scope.isLDAPFormValid = isLDAPFormValid; + function isLDAPFormValid() { + const ldapSettings = $scope.formValues.ldap.serverType === 2 ? $scope.formValues.ldap.adSettings : $scope.formValues.ldap.ldapSettings; + const isTLSMode = ldapSettings.TLSConfig.TLS || ldapSettings.StartTLS; + + return ( + _.compact(ldapSettings.URLs).length && + (ldapSettings.AnonymousMode || (ldapSettings.ReaderDN && ldapSettings.Password)) && + (!isTLSMode || $scope.formValues.TLSCACert || ldapSettings.TLSConfig.TLSSkipVerify) + ); + } + + $scope.isOAuthTeamMembershipFormValid = isOAuthTeamMembershipFormValid; + function isOAuthTeamMembershipFormValid() { + if ($scope.settings && $scope.settings.OAuthSettings.OAuthAutoMapTeamMemberships) { + if (!$scope.settings.OAuthSettings.TeamMemberships.OAuthClaimName) { + return false; + } + const hasInvalidMapping = $scope.settings.OAuthSettings.TeamMemberships.OAuthClaimMappings.some((m) => !(m.ClaimValRegex && m.Team)); + if (hasInvalidMapping) { + return false; + } + } + return true; + } + + function initView() { + $q.all({ + settings: SettingsService.settings(), + teams: TeamService.teams(), + }) + .then(function success(data) { + var settings = data.settings; + $scope.teams = data.teams; + $scope.settings = settings; + + $scope.OAuthSettings = settings.OAuthSettings; + $scope.authMethod = settings.AuthenticationMethod; + if (settings.AuthenticationMethod === 2 && settings.LDAPSettings.ServerType === 2) { + $scope.authMethod = 4; + } + + $scope.formValues.ldap.serverType = settings.LDAPSettings.ServerType; + if (settings.LDAPSettings.ServerType === 2) { + $scope.formValues.ldap.adSettings = settings.LDAPSettings; + } else { + $scope.formValues.ldap.ldapSettings = settings.LDAPSettings; + } + + if (settings.LDAPSettings.URL) { + settings.LDAPSettings.URLs = [settings.LDAPSettings.URL]; + } + if (!settings.LDAPSettings.URLs) { + settings.LDAPSettings.URLs = []; + } + if (!settings.LDAPSettings.URLs.length) { + settings.LDAPSettings.URLs.push(''); + } + if (!settings.LDAPSettings.ServerType) { + settings.LDAPSettings.ServerType = 0; + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + }); + } + + initView(); +}