From 2091932b812a5d9384d25888201442eac585df5f Mon Sep 17 00:00:00 2001 From: spaced Date: Fri, 14 Jun 2024 10:39:13 +0200 Subject: [PATCH 01/16] allow to configure logback json --- ebics-rest-api/pom.xml | 12 ++++++------ .../configuration/EbicsRestConfiguration.kt | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ebics-rest-api/pom.xml b/ebics-rest-api/pom.xml index 109c7237..220f1485 100644 --- a/ebics-rest-api/pom.xml +++ b/ebics-rest-api/pom.xml @@ -15,6 +15,7 @@ 3.0.1 2.2.6 + 7.4 @@ -78,12 +79,6 @@ spring-boot-starter-tomcat provided - - org.apache.maven.plugins - maven-war-plugin - 3.3.1 - maven-plugin - org.jetbrains.kotlin kotlin-maven-allopen @@ -119,6 +114,11 @@ ${spring.restdocs.version} test + + net.logstash.logback + logstash-logback-encoder + ${logstash-logback-encoder.version} + diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/configuration/EbicsRestConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/configuration/EbicsRestConfiguration.kt index 76cc65d3..b12367b3 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/configuration/EbicsRestConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/configuration/EbicsRestConfiguration.kt @@ -28,10 +28,10 @@ class EbicsRestConfiguration( ) : EbicsConfiguration { - final override val locale: Locale = Locale(localeLanguage) + final override val locale: Locale = Locale.of(localeLanguage) init { //Setting default locale as well in order to set locale for Messages singleton object Locale.setDefault(locale) } -} \ No newline at end of file +} From e02952fc060807a037e2e0f53adc7fc05a1421b5 Mon Sep 17 00:00:00 2001 From: spaced Date: Mon, 9 Sep 2024 16:35:05 +0200 Subject: [PATCH 02/16] replace deprecated initialiation mode config for sql int and add logging default config --- ebics-rest-api/src/main/resources/application.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ebics-rest-api/src/main/resources/application.yml b/ebics-rest-api/src/main/resources/application.yml index 449adbff..c0726c10 100644 --- a/ebics-rest-api/src/main/resources/application.yml +++ b/ebics-rest-api/src/main/resources/application.yml @@ -19,9 +19,18 @@ spring: #In order to allow hibernate to work with table names without under scores generated by liquibase diff physical-strategy: "org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl" implicit-strategy: "org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl" - datasource: - initialization-mode: "never" + sql: + init: + mode: never servlet: multipart: max-file-size: "100MB" - max-request-size: "100MB" \ No newline at end of file + max-request-size: "100MB" +logging: + pattern: + console: "%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable" + level: + root: INFO + org.ebics: DEBUG + org.springframework.web: DEBUG + org.springframework.security: DEBUG From e39d5a4995b6a727663950135e4072efb4489594 Mon Sep 17 00:00:00 2001 From: spaced Date: Thu, 12 Sep 2024 19:19:44 +0200 Subject: [PATCH 03/16] initial ldap configuration --- ebics-rest-api/README.md | 26 +++++++++++++ ebics-rest-api/pom.xml | 8 ++++ .../ebicsrestapi/SecurityConfiguration.kt | 16 +------- .../ebicsrestapi/ldap/LdapConfiguration.kt | 37 +++++++++++++++++++ .../ebicsrestapi/ldap/LdapSearchProperties.kt | 17 +++++++++ .../src/main/resources/application.yml | 11 ++++++ 6 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt create mode 100644 ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt diff --git a/ebics-rest-api/README.md b/ebics-rest-api/README.md index b5a8c5a3..534f5827 100644 --- a/ebics-rest-api/README.md +++ b/ebics-rest-api/README.md @@ -11,6 +11,32 @@ The config.properties & logback.xml is expected on path $EWC_CONFIG_HOME/config.properties (or config.yaml) $EWC_CONFIG_HOME/logback.xml +## LDAP integration + +### local development +```shell +docker run --rm -p 1389:1389 --env LDAP_ADMIN_USERNAME=admin \ + --env LDAP_ADMIN_PASSWORD=adminpassword \ + --env LDAP_USERS=customuser \ + --env LDAP_PASSWORDS=custompassword bitnami/openldap:latest +``` +with config: +```yaml + spring: + ldap: + base: dc=example,dc=org + urls: ["ldap://localhost:1389"] + username: cn=admin,dc=example,dc=org + password: adminpassword + search: + group: + base: ou=users + filter: member={0} + user: + filter: (uid={0}) +``` + + ## HTTPS Certificate In order to support HTTPS the appropriate certificate sign by verified cert authority must be configured. diff --git a/ebics-rest-api/pom.xml b/ebics-rest-api/pom.xml index 220f1485..17065026 100644 --- a/ebics-rest-api/pom.xml +++ b/ebics-rest-api/pom.xml @@ -69,6 +69,14 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.ldap + spring-ldap-core + + + org.springframework.security + spring-security-ldap + org.springframework.boot spring-boot-starter-test diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt index 02b41de5..860a4d53 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt @@ -8,8 +8,6 @@ import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.invoke -import org.springframework.security.core.userdetails.User -import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.util.matcher.AntPathRequestMatcher @@ -19,19 +17,10 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher @Order(SecurityProperties.BASIC_AUTH_ORDER) class SecurityConfiguration() { - @Bean - fun configure(): InMemoryUserDetailsManager { - return InMemoryUserDetailsManager( - User.withUsername("guest").password("{noop}pass").roles("GUEST").build(), - User.withUsername("user").password("{noop}pass").roles("USER", "GUEST").build(), - User.withUsername("admin").password("{noop}pass").roles("ADMIN", "USER", "GUEST").build() - ) - } @Bean fun filterChainBasic(http: HttpSecurity): SecurityFilterChain { http { - httpBasic { } authorizeRequests { authorize(HttpMethod.GET, "/bankconnections",hasAnyRole("ADMIN", "USER", "GUEST")) authorize(AntPathRequestMatcher( "/bankconnections/{\\d+}/H00{\\d+}/**",HttpMethod.POST.name()),hasAnyRole("USER", "GUEST")) @@ -48,11 +37,10 @@ class SecurityConfiguration() { authorize(HttpMethod.GET, "/user/settings",hasAnyRole("ADMIN", "USER", "GUEST")) authorize(HttpMethod.PUT, "/user/settings",hasAnyRole("ADMIN", "USER", "GUEST")) } - cors { } + cors { } csrf { disable() } - formLogin { disable() } + formLogin { } } return http.build() - //http.httpBasic().and().authorizeRequests().antMatchers("/users", "/").permitAll().anyRequest().authenticated() } } diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt new file mode 100644 index 00000000..ed08f1a9 --- /dev/null +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt @@ -0,0 +1,37 @@ +package org.ebics.client.ebicsrestapi.ldap + + +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.ldap.core.support.BaseLdapPathContextSource +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator + +@Configuration +@EnableConfigurationProperties(LdapSearchProperties::class) +class LdapConfiguration { + + @Bean + fun authorities(contextSource: BaseLdapPathContextSource, searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { + val authorities = DefaultLdapAuthoritiesPopulator(contextSource, searchProperties.group.base) + authorities.setGroupSearchFilter(searchProperties.group.filter) + return authorities + } + + @Bean + fun authenticationManager(contextSource: BaseLdapPathContextSource, + authorities: LdapAuthoritiesPopulator, + searchProperties: LdapSearchProperties + ): AuthenticationManager { + val factory = LdapBindAuthenticationManagerFactory(contextSource) + factory.setUserSearchFilter(searchProperties.user.filter) + factory.setUserSearchBase(searchProperties.user.base) + //factory.setUserDnPatterns("uid={0},ou=users") + factory.setLdapAuthoritiesPopulator(authorities) + return factory.createAuthenticationManager() + } + +} \ No newline at end of file diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt new file mode 100644 index 00000000..cd7e6f61 --- /dev/null +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt @@ -0,0 +1,17 @@ +package org.ebics.client.ebicsrestapi.ldap + + +import org.springframework.boot.context.properties.ConfigurationProperties + + +@ConfigurationProperties(prefix = "spring.ldap.search") +data class LdapSearchProperties ( + val group: LdapSearchPattern, + val user: LdapSearchPattern +) + + +data class LdapSearchPattern( + val base: String = "", + val filter: String +) diff --git a/ebics-rest-api/src/main/resources/application.yml b/ebics-rest-api/src/main/resources/application.yml index c0726c10..b3046299 100644 --- a/ebics-rest-api/src/main/resources/application.yml +++ b/ebics-rest-api/src/main/resources/application.yml @@ -26,6 +26,17 @@ spring: multipart: max-file-size: "100MB" max-request-size: "100MB" + ldap: + base: dc=example,dc=org + urls: ["ldap://localhost:1389"] + username: cn=admin,dc=example,dc=org + password: adminpassword + search: + group: + base: ou=users + filter: member={0} + user: + filter: (uid={0}) logging: pattern: console: "%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable" From 96005e5df8967d285e4c3b047967bbb03ca9ed57 Mon Sep 17 00:00:00 2001 From: spaced Date: Thu, 12 Sep 2024 20:04:49 +0200 Subject: [PATCH 04/16] add ldap role mapping --- .../client/ebicsrestapi/ldap/LdapConfiguration.kt | 15 ++++++++++++++- .../ebicsrestapi/ldap/LdapSearchProperties.kt | 3 ++- ebics-rest-api/src/main/resources/application.yml | 2 ++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt index ed08f1a9..bc44ff9b 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt @@ -7,17 +7,30 @@ import org.springframework.context.annotation.Configuration import org.springframework.ldap.core.support.BaseLdapPathContextSource import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator +import java.util.* + +typealias AuthorityRecord = Map> +typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority? @Configuration @EnableConfigurationProperties(LdapSearchProperties::class) class LdapConfiguration { - @Bean fun authorities(contextSource: BaseLdapPathContextSource, searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { val authorities = DefaultLdapAuthoritiesPopulator(contextSource, searchProperties.group.base) authorities.setGroupSearchFilter(searchProperties.group.filter) + val mapper: AuthorityMapper = { record -> + val roles = record["cn"] + val role = roles?.first() + val mappedRole= searchProperties.mapping?.get(role)?:role + mappedRole?.let{ SimpleGrantedAuthority("ROLE_${mappedRole.uppercase()}") } + } + + authorities.setAuthorityMapper( mapper) return authorities } diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt index cd7e6f61..32d2df2f 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt @@ -7,7 +7,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "spring.ldap.search") data class LdapSearchProperties ( val group: LdapSearchPattern, - val user: LdapSearchPattern + val user: LdapSearchPattern, + val mapping: Map? // mapping of spring-role -> ldap-role ) diff --git a/ebics-rest-api/src/main/resources/application.yml b/ebics-rest-api/src/main/resources/application.yml index b3046299..39e177cc 100644 --- a/ebics-rest-api/src/main/resources/application.yml +++ b/ebics-rest-api/src/main/resources/application.yml @@ -37,6 +37,8 @@ spring: filter: member={0} user: filter: (uid={0}) + mapping: + readers: admin logging: pattern: console: "%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable" From 808537871cb8cc1665c0e482c4705ebec7a1c3c3 Mon Sep 17 00:00:00 2001 From: spaced Date: Mon, 16 Sep 2024 08:59:20 +0200 Subject: [PATCH 05/16] upgrade axios --- ebics-web-ui/package-lock.json | 46 ++++++++++++++++++------- ebics-web-ui/package.json | 2 +- ebics-web-ui/src/components/base-api.ts | 22 ++++++------ 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/ebics-web-ui/package-lock.json b/ebics-web-ui/package-lock.json index a1630fc0..5690d695 100644 --- a/ebics-web-ui/package-lock.json +++ b/ebics-web-ui/package-lock.json @@ -3172,8 +3172,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "at-least-node": { "version": "1.0.0", @@ -3202,11 +3201,30 @@ } }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, "babel-core": { @@ -4057,7 +4075,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -4895,8 +4912,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "depd": { "version": "1.1.2", @@ -6199,7 +6215,8 @@ "follow-redirects": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "dev": true }, "for-in": { "version": "1.0.2", @@ -8764,14 +8781,12 @@ "mime-db": { "version": "1.49.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", - "dev": true + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" }, "mime-types": { "version": "2.1.32", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", - "dev": true, "requires": { "mime-db": "1.49.0" } @@ -10208,6 +10223,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", diff --git a/ebics-web-ui/package.json b/ebics-web-ui/package.json index e7f4c265..815416e9 100644 --- a/ebics-web-ui/package.json +++ b/ebics-web-ui/package.json @@ -19,7 +19,7 @@ "dependencies": { "@quasar/extras": "^1.11.1", "@types/jest": "^27.0.2", - "axios": "^0.21.1", + "axios": "^1.7.7", "core-js": "^3.6.5", "quasar": "^2.1.0", "vue-i18n": "^9.0.0", diff --git a/ebics-web-ui/src/components/base-api.ts b/ebics-web-ui/src/components/base-api.ts index e023c584..ccb2aa5a 100644 --- a/ebics-web-ui/src/components/base-api.ts +++ b/ebics-web-ui/src/components/base-api.ts @@ -31,17 +31,17 @@ function errorResponseToApiError(error: unknown): ApiError { if (isAxiosError(error)) { if (error.response) { const ebicsApiError = error.response?.data; - if (error.config.responseType == 'arraybuffer') { + if (error.config?.responseType == 'arraybuffer') { //In this case we requesed arraybuffer (not JSON object) so the data must be first extracted return arrayBufferToObject(error.response?.data as unknown as ArrayBuffer) as EbicsApiError } else { //We already have proper object EbicsApiError return ebicsApiError; } - } else { + } else { //There is no real response data, but it is still axios error return {timestamp: new Date().toISOString(), message: JSON.stringify(error.message)} as ApiError - } + } } else { //Non axios error return {timestamp: new Date().toISOString(), message: JSON.stringify(error)} as ApiError @@ -64,9 +64,9 @@ export function getFormatedErrorMessage(apiError: ApiError | EbicsApiError | Ebi } } -export function getFormatedErrorCategory(apiError: ApiError | EbicsApiError): string { +export function getFormatedErrorCategory(apiError: ApiError | EbicsApiError): string { if (isEbicsApiError(apiError)) { - return `HTTP ${apiError.httpStatusCode} ${apiError.httpStatusResonPhrase}` + return `HTTP ${apiError.httpStatusCode} ${apiError.httpStatusResonPhrase}` } else { //Its not from REST API, it comes from the frontend itself, for example backend is available return 'Frontend error'; @@ -127,8 +127,8 @@ export default function useBaseAPI() { /** * Indicates if the Error should be threated as OK - * @param apiError - * @returns + * @param apiError + * @returns */ const isTheApiErrorActuallyOk = (apiError: ApiError): boolean => { return (apiError.message.includes('EBICS_NO_DOWNLOAD_DATA_AVAILABLE')); @@ -136,12 +136,12 @@ export default function useBaseAPI() { /** * Return new msg for error which was assesed as OK by isTheApiErrorActuallyOk - * @param apiError - * @param msg - * @returns + * @param apiError + * @param msg + * @returns */ const remapMsgForOkErrors = (apiError: ApiError, msg: string): string => { - if (apiError.message.includes('EBICS_NO_DOWNLOAD_DATA_AVAILABLE')) + if (apiError.message.includes('EBICS_NO_DOWNLOAD_DATA_AVAILABLE')) return 'No download data available on the EBICS server (EBICS_NO_DOWNLOAD_DATA_AVAILABLE)'; else return msg; From d67f6964f021029886d80f46ab34f3b94033e21e Mon Sep 17 00:00:00 2001 From: spaced Date: Mon, 16 Sep 2024 08:59:51 +0200 Subject: [PATCH 06/16] fix typo --- ebics-web-ui/src/components/user-context.ts | 44 ++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/ebics-web-ui/src/components/user-context.ts b/ebics-web-ui/src/components/user-context.ts index 7f9b26e1..9f7498c4 100644 --- a/ebics-web-ui/src/components/user-context.ts +++ b/ebics-web-ui/src/components/user-context.ts @@ -23,7 +23,7 @@ function getAuthSSOBasicTypeFromEnv():boolean { const authenticationType = ref(getAuthTypeFromEnv()); const refreshUserContextByMounth = true; -const ssoDevOverBasic = ref(getAuthSSOBasicTypeFromEnv()); +const ssoDevOverBasic = ref(getAuthSSOBasicTypeFromEnv()); //Reactive http basic credentials object available in browser session //Initally no default password & username to force login, can be changed for dev purposes @@ -95,27 +95,27 @@ export default function useUserContextAPI() { console.log( 'Basic HTTP credentials:' + JSON.stringify(api.defaults.auth) ); - //We have credential, we do login API call to get principal and roles from backend - try { - const response = await api.get('user'); - q.notify({ - color: 'positive', - position: 'bottom-right', - message: 'Authentication successfull', - icon: 'report_problem', - }); - console.log(JSON.stringify(response.data)); - userContext.value = response.data; - userContext.value.time = new Date().toISOString(); - } catch (error) { - userContext.value = undefined; - q.notify({ - color: 'negative', - position: 'bottom-right', - message: `Authentication failed: ${JSON.stringify(error)}`, - icon: 'report_problem', - }); - throw error; + //We have credential, we do login API call to get principal and roles from backend + try { + const response = await api.get('user'); + q.notify({ + color: 'positive', + position: 'bottom-right', + message: 'Authentication successful', + icon: 'report_problem', + }); + console.log(JSON.stringify(response.data)); + userContext.value = response.data; + userContext.value.time = new Date().toISOString(); + } catch (error) { + userContext.value = undefined; + q.notify({ + color: 'negative', + position: 'bottom-right', + message: `Authentication failed: ${JSON.stringify(error)}`, + icon: 'report_problem', + }); + throw error; } } }; From b962d3a2fb1e31bf189a5d6ed58adf4b28d552de Mon Sep 17 00:00:00 2001 From: spaced Date: Mon, 16 Sep 2024 18:25:37 +0200 Subject: [PATCH 07/16] allow login via server and use session cookie --- .../ebicsrestapi/SecurityConfiguration.kt | 3 +- ebics-web-ui/quasar.conf.js | 15 ++- .../src/components/models/user-context.ts | 1 + ebics-web-ui/src/components/user-context.ts | 116 +++++++++++------- ebics-web-ui/src/pages/UserLogin.vue | 10 +- 5 files changed, 87 insertions(+), 58 deletions(-) diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt index 860a4d53..15beab55 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt @@ -39,7 +39,8 @@ class SecurityConfiguration() { } cors { } csrf { disable() } - formLogin { } + formLogin { defaultSuccessUrl("/user", false) } + logout { } } return http.build() } diff --git a/ebics-web-ui/quasar.conf.js b/ebics-web-ui/quasar.conf.js index 40d65c50..a7a40ff5 100644 --- a/ebics-web-ui/quasar.conf.js +++ b/ebics-web-ui/quasar.conf.js @@ -56,15 +56,15 @@ module.exports = configure(function (ctx) { // Full list of options: https://v2.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build build: { env: ctx.dev ? { - API_URL: 'http://localhost:8080/EbicsWebClient', + API_URL: undefined, AUTH_TYPE: 'HTTP_BASIC', AUTH_TYPE_SSO_OVER_BASIC: undefined } : { //Use relative path for PRODUCTION - means FE SPA & BE REST are using same URL API_URL: undefined, - AUTH_TYPE: 'SSO', + AUTH_TYPE: 'SERVER', //Simulates SSO in with statically given HTTP basic credentials (only by AuthenticationType.SSO for dev purposes) - AUTH_TYPE_SSO_OVER_BASIC: 'yes' + AUTH_TYPE_SSO_OVER_BASIC: undefined }, vueRouterMode: 'hash', // available values: 'hash', 'history' @@ -72,7 +72,7 @@ module.exports = configure(function (ctx) { //minify: true, uglifyOptions: { format: { - ascii_only: true // This fixing wrong transpiled unicode chars in regex from sace.js + ascii_only: true // This fixing wrong transpiled unicode chars in regex from sace.js } }, @@ -101,7 +101,12 @@ module.exports = configure(function (ctx) { devServer: { https: false, port: 8081, - open: true // opens browser window automatically + open: true, // opens browser window automatically + proxy: { + context: ['/login', '/user'], + target: 'http://localhost:8080', + changeOrigin: true + } }, // https://v2.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework diff --git a/ebics-web-ui/src/components/models/user-context.ts b/ebics-web-ui/src/components/models/user-context.ts index 0168d40b..d771a6ae 100644 --- a/ebics-web-ui/src/components/models/user-context.ts +++ b/ebics-web-ui/src/components/models/user-context.ts @@ -8,6 +8,7 @@ export interface UserContext { export enum AuthenticationType { SSO = 'Single sign on', + SERVER = 'server', HTTP_BASIC = 'HTTP Basic (username + password)' } diff --git a/ebics-web-ui/src/components/user-context.ts b/ebics-web-ui/src/components/user-context.ts index 9f7498c4..219a99a9 100644 --- a/ebics-web-ui/src/components/user-context.ts +++ b/ebics-web-ui/src/components/user-context.ts @@ -12,6 +12,8 @@ function getAuthTypeFromEnv():AuthenticationType { return AuthenticationType.SSO; case 'HTTP_BASIC': return AuthenticationType.HTTP_BASIC; + case 'SERVER': + return AuthenticationType.SERVER; default: return AuthenticationType.SSO; } @@ -46,32 +48,66 @@ export default function useUserContextAPI() { const router = useRouter(); const resetUserContextData = async () => { - if (authenticationType.value == AuthenticationType.HTTP_BASIC) { - basicCredentials.value = { username: '', password: '' }; - userContext.value = undefined; - await router.push({ path: 'login' }); - q.notify({ - color: 'positive', - position: 'bottom-right', - message: 'Authentication context reseted', - icon: 'report_problem', - }); - } else if (authenticationType.value == AuthenticationType.SSO) { - await refreshUserContextData(); + switch (authenticationType.value) { + case AuthenticationType.SSO: + await refreshUserContextData(); + break; + case AuthenticationType.SERVER: + await api.get('logout'); + default: + basicCredentials.value = {username: '', password: ''}; + userContext.value = undefined; + await router.push({path: 'login'}); + q.notify({ + color: 'positive', + position: 'bottom-right', + message: 'Logged out', + icon: 'report_problem', + }); } }; - const hasCredentials = (): boolean => { - if (authenticationType.value == AuthenticationType.HTTP_BASIC) - return ( - basicCredentials.value.username !== '' && - basicCredentials.value.password !== '' - ); - else if (authenticationType.value == AuthenticationType.SSO) { - return true; - } - return false; - }; + const hasCredentials = (): boolean => + basicCredentials.value.username !== '' && + basicCredentials.value.password !== ''; + + const onSuccessUserContext = (uc: UserContext) => { + userContext.value = uc; + userContext.value.time = new Date().toISOString(); + q.notify({ + color: 'positive', + position: 'bottom-right', + message: 'Authentication successful', + icon: 'report_problem', + }); + } + + const onFailedUserContext = (error: unknown) => { + userContext.value = undefined; + q.notify({ + color: 'negative', + position: 'bottom-right', + message: `Authentication failed: ${JSON.stringify(error)}`, + icon: 'report_problem', + }); + } + + const login = async (): Promise => { + if (authenticationType.value == AuthenticationType.SERVER) { + try { + const formData = new FormData(); + formData.set('username', basicCredentials.value.username); + formData.set('password', basicCredentials.value.password); + const response = await api.post('login', formData, {headers: {'Content-Type': 'application/x-www-form-urlencoded'}}); + if (response.status == 200) { + onSuccessUserContext(response.data); + } + } catch (error) { + onFailedUserContext(error) + } + } else await refreshUserContextData() + } + const refreshUserContextData = async (): Promise => { if ( @@ -92,30 +128,15 @@ export default function useUserContextAPI() { api.defaults.auth = { username: 'admin', password: 'pass' }; } - console.log( - 'Basic HTTP credentials:' + JSON.stringify(api.defaults.auth) - ); - //We have credential, we do login API call to get principal and roles from backend - try { - const response = await api.get('user'); - q.notify({ - color: 'positive', - position: 'bottom-right', - message: 'Authentication successful', - icon: 'report_problem', - }); - console.log(JSON.stringify(response.data)); - userContext.value = response.data; - userContext.value.time = new Date().toISOString(); - } catch (error) { - userContext.value = undefined; - q.notify({ - color: 'negative', - position: 'bottom-right', - message: `Authentication failed: ${JSON.stringify(error)}`, - icon: 'report_problem', - }); - throw error; + //We have credential, we do user API call to get principal and roles from backend + try { + const response = await api.get('user'); + onSuccessUserContext(response.data); + } catch (error) { + if (authenticationType.value == AuthenticationType.SERVER) { + await router.push({ path: 'login' }); + } else onFailedUserContext(error); + throw error; } } }; @@ -165,5 +186,6 @@ export default function useUserContextAPI() { hasRoleAdmin, resetUserContextData, refreshUserContextData, + login }; } diff --git a/ebics-web-ui/src/pages/UserLogin.vue b/ebics-web-ui/src/pages/UserLogin.vue index 1554e7ae..d3850d6b 100644 --- a/ebics-web-ui/src/pages/UserLogin.vue +++ b/ebics-web-ui/src/pages/UserLogin.vue @@ -7,7 +7,7 @@ filled v-model="basicCredentials.username" label="User name" - hint="User name used for login with HTTP simple authorization" + hint="User name used for login" lazy-rules :rules="[ (val) => @@ -21,7 +21,7 @@ v-model="basicCredentials.password" type="password" label="User password" - hint="User password used for login with HTTP simple authorization" + hint="User password used for login" lazy-rules :rules="[ (val) => @@ -47,7 +47,7 @@ export default defineComponent({ methods: { async onLogin() { try { - await this.refreshUserContextData() + await this.login() await this.$router.push({path: '/userctx'}) } catch(error) { console.log(JSON.stringify(error)) @@ -55,8 +55,8 @@ export default defineComponent({ }, }, setup() { - const { basicCredentials, refreshUserContextData } = useUserContextAPI(); - return { basicCredentials, refreshUserContextData }; + const { basicCredentials, login } = useUserContextAPI(); + return { basicCredentials, login }; }, }); From cd39b000f40cfbd0bc3cc466ad2e6202bd86ee42 Mon Sep 17 00:00:00 2001 From: spaced Date: Wed, 18 Sep 2024 12:25:21 +0200 Subject: [PATCH 08/16] expose ssl http port --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 786f80ac..3879129b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ COPY --from=build /app/ebics-rest-api/target/ebics*.war /app/ WORKDIR /app #remove version form jar files in container and note the used version EXPOSE 8080 +EXPOSE 8443 RUN FN=`(ls ebics-rest-api-*[0-9].war | head -1)`; echo $FN; mv $FN ebics-rest-api.war; touch $FN.version ENTRYPOINT ["java","-jar","ebics-rest-api.war"] From 9a2fb3e02dc23419ae794a61bcbd09d434fd8e65 Mon Sep 17 00:00:00 2001 From: spaced Date: Wed, 18 Sep 2024 12:25:57 +0200 Subject: [PATCH 09/16] add other endpoints for proxy --- ebics-web-ui/quasar.conf.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ebics-web-ui/quasar.conf.js b/ebics-web-ui/quasar.conf.js index a7a40ff5..8fec6495 100644 --- a/ebics-web-ui/quasar.conf.js +++ b/ebics-web-ui/quasar.conf.js @@ -56,7 +56,7 @@ module.exports = configure(function (ctx) { // Full list of options: https://v2.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build build: { env: ctx.dev ? { - API_URL: undefined, + API_URL: "http://localhost:8081", AUTH_TYPE: 'HTTP_BASIC', AUTH_TYPE_SSO_OVER_BASIC: undefined } : { @@ -103,7 +103,7 @@ module.exports = configure(function (ctx) { port: 8081, open: true, // opens browser window automatically proxy: { - context: ['/login', '/user'], + context: ['/login', '/user','/bankconnections','/banks'], target: 'http://localhost:8080', changeOrigin: true } From dbd71e787ef546192220d6d4678561465591cd05 Mon Sep 17 00:00:00 2001 From: spaced Date: Wed, 18 Sep 2024 12:26:48 +0200 Subject: [PATCH 10/16] allow to use profile==dev with basic auth --- README.md | 12 +++++++++- .../ebicsrestapi/SecurityConfiguration.kt | 24 +++++++++++++++++-- .../ebicsrestapi/ldap/LdapConfiguration.kt | 3 ++- examples/application-ldap.yml | 15 ++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 examples/application-ldap.yml diff --git a/README.md b/README.md index 4870adf6..fcad8498 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ docker pull ghcr.io/spaced/ebics-web-client:master ``` run with ```shell -docker run -p 8080:8080 --rm ghcr.io/spaced/ebics-web-client:master +docker run -p 8080:8080 --rm -e SPRING_PROFILES_ACTIVE=dev ghcr.io/spaced/ebics-web-client:master ``` or run with configuration ```shell @@ -38,6 +38,16 @@ java -jar ebics-rest-api/target/ebics-rest-api-x.y.z.war Use HTTPS with trusted certificates, don't use HTTP for production setups. Based on the way of running (standalone spring boot or tomcat container) you need to adjust config.properties [spring boot HTTPS config](https://docs.spring.io/spring-boot/how-to/webserver.html) or Apache Tomcat HTTPS +### LDAP +``` +spring.ldap.base=dc=example,dc=org +spring.ldap.urls[0]=ldap://localhost:1389 +spring.ldap.username=cn=admin,dc=example,dc=org +spring.ldap.password=adminpassword +spring.ldap.search.group.base=ou=users +spring.ldap.search.mapping.adGroupName=admin +``` + ### Architecture & Functionality ![Architecture](ebics-web-client-architecture.drawio.png) diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt index 15beab55..5700a842 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/SecurityConfiguration.kt @@ -3,23 +3,36 @@ package org.ebics.client.ebicsrestapi import org.springframework.boot.autoconfigure.security.SecurityProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.core.annotation.Order +import org.springframework.core.env.Environment import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.core.userdetails.User +import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.util.matcher.AntPathRequestMatcher @Configuration @EnableWebSecurity -@Order(SecurityProperties.BASIC_AUTH_ORDER) class SecurityConfiguration() { + @Bean + @Profile("dev") + fun configure(): InMemoryUserDetailsManager { + return InMemoryUserDetailsManager( + User.withUsername("guest").password("{noop}pass").roles("GUEST").build(), + User.withUsername("user").password("{noop}pass").roles("USER", "GUEST").build(), + User.withUsername("admin").password("{noop}pass").roles("ADMIN", "USER", "GUEST").build() + ) + } + @Bean - fun filterChainBasic(http: HttpSecurity): SecurityFilterChain { + fun filterChainBasic(http: HttpSecurity, env: Environment): SecurityFilterChain { http { authorizeRequests { authorize(HttpMethod.GET, "/bankconnections",hasAnyRole("ADMIN", "USER", "GUEST")) @@ -42,6 +55,13 @@ class SecurityConfiguration() { formLogin { defaultSuccessUrl("/user", false) } logout { } } + if (env.activeProfiles.contains("dev")) { + http { + formLogin { disable() } + logout { disable() } + httpBasic { } + } + } return http.build() } } diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt index bc44ff9b..585d92e5 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt @@ -4,6 +4,7 @@ package org.ebics.client.ebicsrestapi.ldap import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.ldap.core.support.BaseLdapPathContextSource import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory @@ -17,6 +18,7 @@ typealias AuthorityRecord = Map> typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority? @Configuration +@Profile("!dev") @EnableConfigurationProperties(LdapSearchProperties::class) class LdapConfiguration { @Bean @@ -42,7 +44,6 @@ class LdapConfiguration { val factory = LdapBindAuthenticationManagerFactory(contextSource) factory.setUserSearchFilter(searchProperties.user.filter) factory.setUserSearchBase(searchProperties.user.base) - //factory.setUserDnPatterns("uid={0},ou=users") factory.setLdapAuthoritiesPopulator(authorities) return factory.createAuthenticationManager() } diff --git a/examples/application-ldap.yml b/examples/application-ldap.yml new file mode 100644 index 00000000..37c01b47 --- /dev/null +++ b/examples/application-ldap.yml @@ -0,0 +1,15 @@ +--- +spring: + ldap: + base: dc=example,dc=org + urls: ["ldap://localhost:1389"] + username: cn=admin,dc=example,dc=org + password: adminpassword + search: + group: + base: ou=users + filter: member={0} + user: + filter: (uid={0}) + mapping: + readers: admin From a40d7ae9fba47dc06c406c9434da1186eeefb7e6 Mon Sep 17 00:00:00 2001 From: spaced Date: Thu, 19 Sep 2024 10:07:55 +0200 Subject: [PATCH 11/16] better ldap defaulting and do not set ldap stuff in application yml --- .../ebicsrestapi/ldap/LdapSearchProperties.kt | 6 +++--- ebics-rest-api/src/main/resources/application.yml | 14 +------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt index 32d2df2f..8af196e6 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt @@ -6,13 +6,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "spring.ldap.search") data class LdapSearchProperties ( - val group: LdapSearchPattern, - val user: LdapSearchPattern, + val group: LdapSearchPattern = LdapSearchPattern("","member={0}"), + val user: LdapSearchPattern = LdapSearchPattern("","(uid={0})"), val mapping: Map? // mapping of spring-role -> ldap-role ) data class LdapSearchPattern( val base: String = "", - val filter: String + val filter: String = "" ) diff --git a/ebics-rest-api/src/main/resources/application.yml b/ebics-rest-api/src/main/resources/application.yml index 39e177cc..17d3b70c 100644 --- a/ebics-rest-api/src/main/resources/application.yml +++ b/ebics-rest-api/src/main/resources/application.yml @@ -26,19 +26,7 @@ spring: multipart: max-file-size: "100MB" max-request-size: "100MB" - ldap: - base: dc=example,dc=org - urls: ["ldap://localhost:1389"] - username: cn=admin,dc=example,dc=org - password: adminpassword - search: - group: - base: ou=users - filter: member={0} - user: - filter: (uid={0}) - mapping: - readers: admin + logging: pattern: console: "%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable" From e72cb1ca01b8c2d4474de0696aefe56a86ce9f46 Mon Sep 17 00:00:00 2001 From: spaced Date: Thu, 26 Sep 2024 18:03:28 +0200 Subject: [PATCH 12/16] allow to configure ldap for active directory --- ebics-rest-api/README.md | 1 + ebics-rest-api/pom.xml | 4 +++ .../ebicsrestapi/ldap/LdapConfiguration.kt | 36 ++++++++++++++----- .../ebicsrestapi/ldap/LdapSearchProperties.kt | 1 + pom.xml | 2 +- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/ebics-rest-api/README.md b/ebics-rest-api/README.md index 534f5827..95b821f8 100644 --- a/ebics-rest-api/README.md +++ b/ebics-rest-api/README.md @@ -25,6 +25,7 @@ with config: spring: ldap: base: dc=example,dc=org + domain: example.com # for active directory urls: ["ldap://localhost:1389"] username: cn=admin,dc=example,dc=org password: adminpassword diff --git a/ebics-rest-api/pom.xml b/ebics-rest-api/pom.xml index 17065026..b11fd319 100644 --- a/ebics-rest-api/pom.xml +++ b/ebics-rest-api/pom.xml @@ -127,6 +127,10 @@ logstash-logback-encoder ${logstash-logback-encoder.version} + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt index 585d92e5..b575c59e 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt @@ -1,6 +1,7 @@ package org.ebics.client.ebicsrestapi.ldap +import org.springframework.boot.autoconfigure.ldap.LdapProperties import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -10,9 +11,9 @@ import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator -import java.util.* typealias AuthorityRecord = Map> typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority? @@ -21,25 +22,31 @@ typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority? @Profile("!dev") @EnableConfigurationProperties(LdapSearchProperties::class) class LdapConfiguration { + @Bean - fun authorities(contextSource: BaseLdapPathContextSource, searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { + fun authorities( + contextSource: BaseLdapPathContextSource, + searchProperties: LdapSearchProperties + ): LdapAuthoritiesPopulator { val authorities = DefaultLdapAuthoritiesPopulator(contextSource, searchProperties.group.base) authorities.setGroupSearchFilter(searchProperties.group.filter) val mapper: AuthorityMapper = { record -> val roles = record["cn"] val role = roles?.first() - val mappedRole= searchProperties.mapping?.get(role)?:role - mappedRole?.let{ SimpleGrantedAuthority("ROLE_${mappedRole.uppercase()}") } + val mappedRole = searchProperties.mapping?.get(role) ?: role + mappedRole?.let { SimpleGrantedAuthority("ROLE_${mappedRole.uppercase()}") } } - authorities.setAuthorityMapper( mapper) + authorities.setAuthorityMapper(mapper) return authorities } @Bean - fun authenticationManager(contextSource: BaseLdapPathContextSource, - authorities: LdapAuthoritiesPopulator, - searchProperties: LdapSearchProperties + @Profile("openldap") + fun authenticationManager( + contextSource: BaseLdapPathContextSource, + authorities: LdapAuthoritiesPopulator, + searchProperties: LdapSearchProperties ): AuthenticationManager { val factory = LdapBindAuthenticationManagerFactory(contextSource) factory.setUserSearchFilter(searchProperties.user.filter) @@ -48,4 +55,17 @@ class LdapConfiguration { return factory.createAuthenticationManager() } + @Bean + fun authenticationProvider( + ldapProperties: LdapProperties, + searchProperties: LdapSearchProperties + ): ActiveDirectoryLdapAuthenticationProvider { + return ActiveDirectoryLdapAuthenticationProvider( + searchProperties.domain, + ldapProperties.urls.get(0), + ldapProperties.base + ) + + } + } \ No newline at end of file diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt index 8af196e6..f2da0fea 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt @@ -6,6 +6,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "spring.ldap.search") data class LdapSearchProperties ( + val domain: String = "", val group: LdapSearchPattern = LdapSearchPattern("","member={0}"), val user: LdapSearchPattern = LdapSearchPattern("","(uid={0})"), val mapping: Map? // mapping of spring-role -> ldap-role diff --git a/pom.xml b/pom.xml index 2ea0ffb2..6f8d4c28 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ 21 1.9.24 1.9 - 3.3.0 + 3.3.4 UTF-8 4.0.2 2.22.2 From df986a7389cdfd03bdf91956b0ca9351bad1ce06 Mon Sep 17 00:00:00 2001 From: spaced Date: Thu, 26 Sep 2024 20:36:10 +0200 Subject: [PATCH 13/16] map active directory memberOf to spring role --- .../ActiveDirectoryRoleMapperPopulator.kt | 37 +++++++++++++++++++ .../ebicsrestapi/ldap/LdapConfiguration.kt | 32 ++++++---------- 2 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt new file mode 100644 index 00000000..7ce3e275 --- /dev/null +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt @@ -0,0 +1,37 @@ +package org.ebics.client.ebicsrestapi.ldap + +import org.slf4j.LoggerFactory +import org.springframework.ldap.core.DirContextOperations +import org.springframework.ldap.core.DistinguishedName +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.AuthorityUtils +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator + + +/** + * Translates ad memberOf attribute to role based on ldap search property [LdapSearchProperties.mapping] + * inspired by [DefaultActiveDirectoryAuthoritiesPopulator] + */ +class ActiveDirectoryRoleMapperPopulator(val mapping: Map?) : LdapAuthoritiesPopulator { + private val logger = LoggerFactory.getLogger(ActiveDirectoryRoleMapperPopulator::class.java) + override fun getGrantedAuthorities( + userData: DirContextOperations?, + username: String? + ): Collection? { + val groups = userData?.getStringAttributes("memberOf") + if (groups == null) { + logger.debug("No values for 'memberOf' attribute."); + return AuthorityUtils.NO_AUTHORITIES; + } + if (logger.isDebugEnabled) logger.debug("'memberOf' attribute values: " + groups.asList()); + + return buildList { + for (group in groups) { + val mappedRole = mapping?.get(DistinguishedName(group).removeLast().value) + if (mappedRole != null) add(SimpleGrantedAuthority("ROLE_${mappedRole.uppercase()}")) + } + } + } + +} \ No newline at end of file diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt index b575c59e..d776aacf 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt @@ -1,6 +1,5 @@ package org.ebics.client.ebicsrestapi.ldap - import org.springframework.boot.autoconfigure.ldap.LdapProperties import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean @@ -24,10 +23,8 @@ typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority? class LdapConfiguration { @Bean - fun authorities( - contextSource: BaseLdapPathContextSource, - searchProperties: LdapSearchProperties - ): LdapAuthoritiesPopulator { + @Profile("openldap") + fun authorities(contextSource: BaseLdapPathContextSource, searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { val authorities = DefaultLdapAuthoritiesPopulator(contextSource, searchProperties.group.base) authorities.setGroupSearchFilter(searchProperties.group.filter) val mapper: AuthorityMapper = { record -> @@ -41,13 +38,14 @@ class LdapConfiguration { return authorities } + @Bean + fun activeDirectoryAuthorities(searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { + return ActiveDirectoryRoleMapperPopulator(searchProperties.mapping) + } + @Bean @Profile("openldap") - fun authenticationManager( - contextSource: BaseLdapPathContextSource, - authorities: LdapAuthoritiesPopulator, - searchProperties: LdapSearchProperties - ): AuthenticationManager { + fun authenticationManager(contextSource: BaseLdapPathContextSource, authorities: LdapAuthoritiesPopulator, searchProperties: LdapSearchProperties): AuthenticationManager { val factory = LdapBindAuthenticationManagerFactory(contextSource) factory.setUserSearchFilter(searchProperties.user.filter) factory.setUserSearchBase(searchProperties.user.base) @@ -56,16 +54,10 @@ class LdapConfiguration { } @Bean - fun authenticationProvider( - ldapProperties: LdapProperties, - searchProperties: LdapSearchProperties - ): ActiveDirectoryLdapAuthenticationProvider { - return ActiveDirectoryLdapAuthenticationProvider( - searchProperties.domain, - ldapProperties.urls.get(0), - ldapProperties.base - ) - + fun authenticationProvider(ldapProperties: LdapProperties, searchProperties: LdapSearchProperties, authorities: LdapAuthoritiesPopulator): ActiveDirectoryLdapAuthenticationProvider { + val adProvider = ActiveDirectoryLdapAuthenticationProvider(searchProperties.domain, ldapProperties.urls[0], ldapProperties.base) + adProvider.setAuthoritiesPopulator(authorities) + return adProvider } } \ No newline at end of file From d94349c4647205116bbc8b068653a275bbce0f49 Mon Sep 17 00:00:00 2001 From: spaced Date: Fri, 27 Sep 2024 16:16:23 +0200 Subject: [PATCH 14/16] allow login using a (admin-)user for search the user and authenticate user with bind --- .../org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt index d776aacf..7d1f8661 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt @@ -23,7 +23,7 @@ typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority? class LdapConfiguration { @Bean - @Profile("openldap") + @Profile("ldap-search") fun authorities(contextSource: BaseLdapPathContextSource, searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { val authorities = DefaultLdapAuthoritiesPopulator(contextSource, searchProperties.group.base) authorities.setGroupSearchFilter(searchProperties.group.filter) @@ -44,7 +44,6 @@ class LdapConfiguration { } @Bean - @Profile("openldap") fun authenticationManager(contextSource: BaseLdapPathContextSource, authorities: LdapAuthoritiesPopulator, searchProperties: LdapSearchProperties): AuthenticationManager { val factory = LdapBindAuthenticationManagerFactory(contextSource) factory.setUserSearchFilter(searchProperties.user.filter) @@ -54,6 +53,7 @@ class LdapConfiguration { } @Bean + @Profile("ldap-ad") fun authenticationProvider(ldapProperties: LdapProperties, searchProperties: LdapSearchProperties, authorities: LdapAuthoritiesPopulator): ActiveDirectoryLdapAuthenticationProvider { val adProvider = ActiveDirectoryLdapAuthenticationProvider(searchProperties.domain, ldapProperties.urls[0], ldapProperties.base) adProvider.setAuthoritiesPopulator(authorities) From 3335ceff1c2eef669a5f53e27da20de4ba6cf7cb Mon Sep 17 00:00:00 2001 From: spaced Date: Sat, 28 Sep 2024 12:48:16 +0200 Subject: [PATCH 15/16] allow to map multiple roles per ad group --- .../ldap/ActiveDirectoryRoleMapperPopulator.kt | 6 +++--- .../client/ebicsrestapi/ldap/LdapConfiguration.kt | 10 ++++++---- .../client/ebicsrestapi/ldap/LdapSearchProperties.kt | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt index 7ce3e275..550096c6 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/ActiveDirectoryRoleMapperPopulator.kt @@ -13,7 +13,7 @@ import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator * Translates ad memberOf attribute to role based on ldap search property [LdapSearchProperties.mapping] * inspired by [DefaultActiveDirectoryAuthoritiesPopulator] */ -class ActiveDirectoryRoleMapperPopulator(val mapping: Map?) : LdapAuthoritiesPopulator { +class ActiveDirectoryRoleMapperPopulator(val mapping: Map>?) : LdapAuthoritiesPopulator { private val logger = LoggerFactory.getLogger(ActiveDirectoryRoleMapperPopulator::class.java) override fun getGrantedAuthorities( userData: DirContextOperations?, @@ -28,8 +28,8 @@ class ActiveDirectoryRoleMapperPopulator(val mapping: Map?) : Lda return buildList { for (group in groups) { - val mappedRole = mapping?.get(DistinguishedName(group).removeLast().value) - if (mappedRole != null) add(SimpleGrantedAuthority("ROLE_${mappedRole.uppercase()}")) + val mappedRoles = mapping?.get(DistinguishedName(group).removeLast().value) + if (mappedRoles != null) mappedRoles.forEach{ r:String -> add(SimpleGrantedAuthority("ROLE_${r.uppercase()}"))} } } } diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt index 7d1f8661..83e61248 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapConfiguration.kt @@ -23,15 +23,15 @@ typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority? class LdapConfiguration { @Bean - @Profile("ldap-search") + @Profile("ldap-auth-group-search") fun authorities(contextSource: BaseLdapPathContextSource, searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { val authorities = DefaultLdapAuthoritiesPopulator(contextSource, searchProperties.group.base) authorities.setGroupSearchFilter(searchProperties.group.filter) val mapper: AuthorityMapper = { record -> val roles = record["cn"] val role = roles?.first() - val mappedRole = searchProperties.mapping?.get(role) ?: role - mappedRole?.let { SimpleGrantedAuthority("ROLE_${mappedRole.uppercase()}") } + val mappedRole = searchProperties.mapping?.get(role) + mappedRole?.first()?.let { r -> SimpleGrantedAuthority("ROLE_${r.uppercase()}") } } authorities.setAuthorityMapper(mapper) @@ -39,11 +39,13 @@ class LdapConfiguration { } @Bean + @Profile("default", "ldap-auth-ad-memberof") fun activeDirectoryAuthorities(searchProperties: LdapSearchProperties): LdapAuthoritiesPopulator { return ActiveDirectoryRoleMapperPopulator(searchProperties.mapping) } @Bean + @Profile("default", "ldap-bind-default") fun authenticationManager(contextSource: BaseLdapPathContextSource, authorities: LdapAuthoritiesPopulator, searchProperties: LdapSearchProperties): AuthenticationManager { val factory = LdapBindAuthenticationManagerFactory(contextSource) factory.setUserSearchFilter(searchProperties.user.filter) @@ -53,7 +55,7 @@ class LdapConfiguration { } @Bean - @Profile("ldap-ad") + @Profile("ldap-bind-ad") fun authenticationProvider(ldapProperties: LdapProperties, searchProperties: LdapSearchProperties, authorities: LdapAuthoritiesPopulator): ActiveDirectoryLdapAuthenticationProvider { val adProvider = ActiveDirectoryLdapAuthenticationProvider(searchProperties.domain, ldapProperties.urls[0], ldapProperties.base) adProvider.setAuthoritiesPopulator(authorities) diff --git a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt index f2da0fea..33670257 100644 --- a/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt +++ b/ebics-rest-api/src/main/kotlin/org/ebics/client/ebicsrestapi/ldap/LdapSearchProperties.kt @@ -9,7 +9,7 @@ data class LdapSearchProperties ( val domain: String = "", val group: LdapSearchPattern = LdapSearchPattern("","member={0}"), val user: LdapSearchPattern = LdapSearchPattern("","(uid={0})"), - val mapping: Map? // mapping of spring-role -> ldap-role + val mapping: Map>? // mapping of spring-role -> ldap-role ) From 597275a8375f9ed3c730173ba58a355c6eafcbba Mon Sep 17 00:00:00 2001 From: spaced Date: Sat, 28 Sep 2024 12:48:26 +0200 Subject: [PATCH 16/16] update readmes --- README.md | 16 +++-------- ebics-rest-api/README.md | 60 ++++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index fcad8498..6c4f9b2b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # EBICS Web Client -EBICS Web Client is a web UI which is used for exchanging of payments files via EBICS connection with the bank(s) supporting [EBICS protocol](https://www.ebics.de). - +EBICS Web Client is a web application which is used for exchanging of payments files via EBICS connection with the bank(s) supporting [EBICS protocol](https://www.ebics.de). +Support of following EBICS versions: +- EBICS 2.5 (H004) +- EBICS 3.0 (H005) ## Installing/running using Docker @@ -38,16 +40,6 @@ java -jar ebics-rest-api/target/ebics-rest-api-x.y.z.war Use HTTPS with trusted certificates, don't use HTTP for production setups. Based on the way of running (standalone spring boot or tomcat container) you need to adjust config.properties [spring boot HTTPS config](https://docs.spring.io/spring-boot/how-to/webserver.html) or Apache Tomcat HTTPS -### LDAP -``` -spring.ldap.base=dc=example,dc=org -spring.ldap.urls[0]=ldap://localhost:1389 -spring.ldap.username=cn=admin,dc=example,dc=org -spring.ldap.password=adminpassword -spring.ldap.search.group.base=ou=users -spring.ldap.search.mapping.adGroupName=admin -``` - ### Architecture & Functionality ![Architecture](ebics-web-client-architecture.drawio.png) diff --git a/ebics-rest-api/README.md b/ebics-rest-api/README.md index 95b821f8..b2a14234 100644 --- a/ebics-rest-api/README.md +++ b/ebics-rest-api/README.md @@ -15,28 +15,54 @@ $EWC_CONFIG_HOME/logback.xml ### local development ```shell -docker run --rm -p 1389:1389 --env LDAP_ADMIN_USERNAME=admin \ +docker run --rm -p 1389:1389 --env LDAP_ADMIN_USERNAME=admin \ --env LDAP_ADMIN_PASSWORD=adminpassword \ --env LDAP_USERS=customuser \ --env LDAP_PASSWORDS=custompassword bitnami/openldap:latest ``` -with config: -```yaml - spring: - ldap: - base: dc=example,dc=org - domain: example.com # for active directory - urls: ["ldap://localhost:1389"] - username: cn=admin,dc=example,dc=org - password: adminpassword - search: - group: - base: ou=users - filter: member={0} - user: - filter: (uid={0}) -``` +### Configuration +#### spring profiles for Authentication +| profile | description | +| --------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|ldap-bind-ad| Authenticate with Active Directory. Typically, authentication is performed by using the domain username (in the form of user@domain), rather than using an LDAP distinguished name. Property `spring.ldap.search.domain` is required. | +|ldap-bind-default| Authenticate with bind LDAP distinguished name| +#### spring profiles for Authorization +| profile | description | +|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ldap-auth-ad-memberof | Maps attribute field `memberOf` with role using property mapping `spring.ldap.search.mapping.${groupName}=${roleNames}` | +| ldap-auth-group-search| Search with `spring.ldap.search.group.base` and `spring.ldap.search.group.filter` and maps to role using property mapping `spring.ldap.search.mapping.${groupName}=${roleName}` | +### examples +openldap +```properties +spring.profiles.active=ldap-bind-default,ldap-auth-group-search +spring.ldap.urls=ldap://localhost:1389 +spring.ldap.base=dc=example,dc=org +spring.ldap.username=cn=admin,dc=example,dc=org +spring.ldap.password=adminpassword +spring.ldap.search.user.filter=(uid={0}) +spring.ldap.search.group.base=ou=users +spring.ldap.search.group.filter=member={0} +spring.ldap.search.mapping.readers=admin +``` +openldap proxy to active directory +```properties +spring.profiles.active=ldap-bind-default,ldap-auth-ad-memberof +spring.ldap.urls=ldap://localhost:1389 +spring.ldap.base=dc=example,dc=org +spring.ldap.username=cn=admin,dc=example,dc=org +spring.ldap.password=adminpassword +spring.ldap.search.user.filter=(&(objectClass=user)(sAMAccountName={0})) +spring.ldap.search.mapping.readers=admin +``` +active directory +```properties +spring.profiles.active=ldap-bind-ad,ldap-auth-ad-memberof +spring.ldap.urls=ldap://localhost:1389 +spring.ldap.base=dc=example,dc=org +spring.ldap.search.domain=example.com +spring.ldap.search.mapping.readers=admin +``` ## HTTPS Certificate