Skip to content

Commit

Permalink
Merge pull request #1 from spaced/feature-ldap
Browse files Browse the repository at this point in the history
Feature ldap
  • Loading branch information
spaced authored Sep 28, 2024
2 parents a011780 + 597275a commit 57e2256
Show file tree
Hide file tree
Showing 19 changed files with 371 additions and 100 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -12,7 +14,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
Expand Down
53 changes: 53 additions & 0 deletions ebics-rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,59 @@ 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
```
### 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

In order to support HTTPS the appropriate certificate sign by verified cert authority must be configured.
Expand Down
24 changes: 18 additions & 6 deletions ebics-rest-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<properties>
<spring.restdocs.version>3.0.1</spring.restdocs.version>
<asciidoctor-maven-plugin-version>2.2.6</asciidoctor-maven-plugin-version>
<logstash-logback-encoder.version>7.4</logstash-logback-encoder.version>
</properties>
<dependencyManagement>
<dependencies>
Expand Down Expand Up @@ -68,6 +69,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand All @@ -78,12 +87,6 @@
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency-->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
<type>maven-plugin</type>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
Expand Down Expand Up @@ -119,6 +122,15 @@
<version>${spring.restdocs.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>${logstash-logback-encoder.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ 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
Expand All @@ -16,10 +18,10 @@ 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(),
Expand All @@ -28,10 +30,10 @@ class SecurityConfiguration() {
)
}


@Bean
fun filterChainBasic(http: HttpSecurity): SecurityFilterChain {
fun filterChainBasic(http: HttpSecurity, env: Environment): SecurityFilterChain {
http {
httpBasic { }
authorizeRequests {
authorize(HttpMethod.GET, "/bankconnections",hasAnyRole("ADMIN", "USER", "GUEST"))
authorize(AntPathRequestMatcher( "/bankconnections/{\\d+}/H00{\\d+}/**",HttpMethod.POST.name()),hasAnyRole("USER", "GUEST"))
Expand All @@ -48,11 +50,18 @@ 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 { defaultSuccessUrl("/user", false) }
logout { }
}
if (env.activeProfiles.contains("dev")) {
http {
formLogin { disable() }
logout { disable() }
httpBasic { }
}
}
return http.build()
//http.httpBasic().and().authorizeRequests().antMatchers("/users", "/").permitAll().anyRequest().authenticated()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String,Array<String>>?) : LdapAuthoritiesPopulator {
private val logger = LoggerFactory.getLogger(ActiveDirectoryRoleMapperPopulator::class.java)
override fun getGrantedAuthorities(
userData: DirContextOperations?,
username: String?
): Collection<GrantedAuthority?>? {
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 mappedRoles = mapping?.get(DistinguishedName(group).removeLast().value)
if (mappedRoles != null) mappedRoles.forEach{ r:String -> add(SimpleGrantedAuthority("ROLE_${r.uppercase()}"))}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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
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
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

typealias AuthorityRecord = Map<String, List<String>>
typealias AuthorityMapper = (AuthorityRecord) -> GrantedAuthority?

@Configuration
@Profile("!dev")
@EnableConfigurationProperties(LdapSearchProperties::class)
class LdapConfiguration {

@Bean
@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)
mappedRole?.first()?.let { r -> SimpleGrantedAuthority("ROLE_${r.uppercase()}") }
}

authorities.setAuthorityMapper(mapper)
return authorities
}

@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)
factory.setUserSearchBase(searchProperties.user.base)
factory.setLdapAuthoritiesPopulator(authorities)
return factory.createAuthenticationManager()
}

@Bean
@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)
return adProvider
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.ebics.client.ebicsrestapi.ldap


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<String,Array<String>>? // mapping of spring-role -> ldap-role
)


data class LdapSearchPattern(
val base: String = "",
val filter: String = ""
)
16 changes: 13 additions & 3 deletions ebics-rest-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ 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"
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
Loading

0 comments on commit 57e2256

Please sign in to comment.