Skip to content

Commit

Permalink
Merge pull request #2 from spaced/feature-apikey
Browse files Browse the repository at this point in the history
Feature apikey
  • Loading branch information
spaced authored Oct 2, 2024
2 parents 57e2256 + a5e5f52 commit a557b20
Show file tree
Hide file tree
Showing 18 changed files with 178 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ enum class BusinessRole {
ROLE_ADMIN,
ROLE_USER,
ROLE_GUEST,
ROLE_API,
ROLE_UNKNOWN;

companion object {
fun fromString(roleName: String): BusinessRole {
return when (roleName) {
"ROLE_ADMIN" -> ROLE_ADMIN
"ROLE_USER" -> ROLE_USER
"ROLE_API" -> ROLE_API
"ROLE_GUEST" -> ROLE_GUEST
else -> ROLE_UNKNOWN
}
Expand Down
11 changes: 11 additions & 0 deletions ebics-rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ The config.properties & logback.xml is expected on path
$EWC_CONFIG_HOME/config.properties (or config.yaml)
$EWC_CONFIG_HOME/logback.xml

## API Key configuration
```properties
ebics.api.clients.FirstClientApiId.key=aVeryLongRandomString
ebics.api.clients.FirstClientApiId.role=admin
```
allows client using http header
```properties
X-App-Id=FirstClientApiId
X-Api-Id=aVeryLongRandomString
```

## LDAP integration

### local development
Expand Down
8 changes: 8 additions & 0 deletions ebics-rest-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,14 @@
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>19</source>
<target>19</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package org.ebics.client.ebicsrestapi

import org.springframework.boot.autoconfigure.security.SecurityProperties
import org.ebics.client.ebicsrestapi.key.ApiKeyAuthenticationFilter
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
Expand All @@ -13,6 +12,7 @@ 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.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher


Expand All @@ -35,19 +35,19 @@ class SecurityConfiguration() {
fun filterChainBasic(http: HttpSecurity, env: Environment): SecurityFilterChain {
http {
authorizeRequests {
authorize(HttpMethod.GET, "/bankconnections",hasAnyRole("ADMIN", "USER", "GUEST"))
authorize(HttpMethod.GET, "/bankconnections",hasAnyRole("ADMIN", "USER", "API", "GUEST"))
authorize(AntPathRequestMatcher( "/bankconnections/{\\d+}/H00{\\d+}/**",HttpMethod.POST.name()),hasAnyRole("USER", "GUEST"))
authorize(AntPathRequestMatcher("/bankconnections/{\\d+}/H00{\\d+}/**", HttpMethod.GET.name()),hasAnyRole("USER", "GUEST"))
authorize(AntPathRequestMatcher("/bankconnections/{\\d+}/H00{\\d+}/**", HttpMethod.GET.name()),hasAnyRole("USER", "API", "GUEST"))
authorize(HttpMethod.POST, "/bankconnections",hasRole("USER"))
authorize(AntPathRequestMatcher("/bankconnections/{\\d+}", HttpMethod.PUT.name()),hasRole("USER"))
authorize(AntPathRequestMatcher("/bankconnections/{\\d+}",HttpMethod.DELETE.name()),hasAnyRole("ADMIN", "USER"))
authorize(HttpMethod.GET, "/banks/**",hasAnyRole("ADMIN", "USER", "GUEST"))
authorize(HttpMethod.GET, "/banks/**",hasAnyRole("ADMIN", "USER", "API", "GUEST"))
authorize(HttpMethod.POST, "/banks/**",hasRole("ADMIN"))
authorize(HttpMethod.PUT, "/banks/**",hasRole("ADMIN"))
authorize(HttpMethod.PATCH, "/banks/**",hasRole("ADMIN"))
authorize(HttpMethod.DELETE, "/banks/**",hasRole("ADMIN"))
authorize(HttpMethod.GET, "/user",hasAnyRole("ADMIN", "USER", "GUEST"))
authorize(HttpMethod.GET, "/user/settings",hasAnyRole("ADMIN", "USER", "GUEST"))
authorize(HttpMethod.GET, "/user",hasAnyRole("ADMIN", "USER", "API", "GUEST"))
authorize(HttpMethod.GET, "/user/settings",hasAnyRole("ADMIN", "USER", "API", "GUEST"))
authorize(HttpMethod.PUT, "/user/settings",hasAnyRole("ADMIN", "USER", "GUEST"))
}
cors { }
Expand All @@ -62,6 +62,7 @@ class SecurityConfiguration() {
httpBasic { }
}
}
http.addFilterBefore(ApiKeyAuthenticationFilter(),UsernamePasswordAuthenticationFilter::class.java)
return http.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import java.util.*

@Component("EbicsBankAPI")
class EbicsBankAPI(
private val configuration: EbicsRestConfiguration,
private val bankService: BankService,
private val versionSupportService: VersionSupportService,
private val bankOperations: BankOperations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import org.ebics.client.ebicsrestapi.bankconnection.UserIdPass
import org.ebics.client.ebicsrestapi.configuration.EbicsRestConfiguration
import org.ebics.client.model.EbicsProduct
import org.ebics.client.model.EbicsSession
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.stereotype.Component

@Component
@EnableConfigurationProperties(EbicsRestConfiguration::class)
class EbicsSessionFactory(
private val userService: BankConnectionService,
private val configuration: EbicsRestConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,23 @@ package org.ebics.client.ebicsrestapi.configuration

import org.ebics.client.api.EbicsConfiguration
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import java.util.*

@Configuration
class EbicsRestConfiguration(

@Value("\${ebics.signatureVersion:A005}")
override val signatureVersion: String,

@Value("\${ebics.authenticationVersion:X002}")
override val authenticationVersion: String,

@Value("\${ebics.encryptionVersion:E002}")
override val encryptionVersion: String,

@Value("\${ebics.trace:#{true}}")
override val isTraceEnabled: Boolean,

@Value("\${ebics.compression:#{true}}")
override val isCompressionEnabled: Boolean,

@Value("\${ebics.locale.language:en}")
private val localeLanguage:String,
@ConfigurationProperties("ebics")
data class EbicsRestConfiguration(
override val signatureVersion: String = "A005",
override val authenticationVersion: String = "X002",
override val encryptionVersion: String = "E002",
val trace: Boolean = true,
val compression: Boolean = true,
private val localeLanguage:String = "en",

) : EbicsConfiguration {

final override val locale: Locale = Locale.of(localeLanguage)
override val locale: Locale = Locale.of(localeLanguage)
override val isTraceEnabled = trace
override val isCompressionEnabled = compression

init {
//Setting default locale as well in order to set locale for Messages singleton object
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.ebics.client.ebicsrestapi.key

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.OncePerRequestFilter

class ApiKeyAuthenticationFilter() : OncePerRequestFilter() {

fun authTokenFromHttpHeader(request: HttpServletRequest): Authentication? {
val providedAppId = request.getHeader("X-App-Id")
val providedApiKey = request.getHeader("X-Api-Key")
if (providedAppId == null || providedApiKey == null) return null
return ApiKeyAuthenticationToken(providedAppId, providedApiKey)
}

override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
authTokenFromHttpHeader(request).let { authentication -> SecurityContextHolder.getContext().authentication = authentication }
filterChain.doFilter(request, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.ebics.client.ebicsrestapi.key

import org.ebics.client.ebicsrestapi.key.ApiKeyProperties.ApiKey
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority

class ApiKeyAuthenticationProvider(val appIds: Map<String, ApiKey>) : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
val auth = authentication as ApiKeyAuthenticationToken
val v = appIds[auth.principal]
if (v == null || v.key != authentication.credentials) throw BadCredentialsException("bad api id or key")

val role = SimpleGrantedAuthority("ROLE_${v.role.uppercase()}")
return ApiKeyAuthenticationToken(auth.appId, auth.apiKey, listOf(role))
}

override fun supports(authentication: Class<*>): Boolean {
return authentication.isAssignableFrom(ApiKeyAuthenticationToken::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.ebics.client.ebicsrestapi.key

import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.AuthorityUtils

/**
* An api key pair authentication
*/
class ApiKeyAuthenticationToken(val appId: String, val apiKey: String, authorities: Collection<out GrantedAuthority?> = AuthorityUtils.NO_AUTHORITIES) : AbstractAuthenticationToken(authorities) {


override fun getPrincipal(): Any? {
return appId
}

override fun getCredentials(): Any? {
return apiKey
}

override fun eraseCredentials() {
super.eraseCredentials()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.ebics.client.ebicsrestapi.key

import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(ApiKeyProperties::class)
class ApiKeyConfiguration {

@Bean
fun apiKeyAuthentication(apiKeyProperties: ApiKeyProperties): ApiKeyAuthenticationProvider {
return ApiKeyAuthenticationProvider(apiKeyProperties.clients)
}

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


import org.springframework.boot.context.properties.ConfigurationProperties

/**
* configuration properties for api keys
* ebics.api.clients.firstAppId.key=aRandomKeyString
* ebics.api.clients.firstAppId.role=admin
*/

@ConfigurationProperties(prefix = "ebics.api")
data class ApiKeyProperties(
val clients: Map<String, ApiKey> = emptyMap()
) {
data class ApiKey(
val key: String,
val role: String = "api"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ data class LdapSearchProperties (
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 = ""
)
) {
data class LdapSearchPattern(
val base: String = "",
val filter: String = ""
)
}
14 changes: 14 additions & 0 deletions ebics-rest-api/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
management:
health:
ldap:
enabled: false

ebics:
api:
clients:
testclient-admin-xy:
key: abc1234
role: admin
testclient-api-xy:
key: abc1234
11 changes: 8 additions & 3 deletions ebics-rest-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
build:
#Use maven build properties in spring, provide those values for application
revision: "@revision@"
Expand All @@ -6,12 +7,16 @@ ebics:
signatureVersion: "A005"
authenticationVersion: "X002"
encryptionVersion: "E002"
isTraceEnabled: "true"
isCompressionEnabled: "true"
trace: "true"
compression: "true"
locale:
language: "en"

spring:
main:
banner-mode: off
jpa:
open-in-view: false
hibernate:
#In order to stop spring boot from maintaining DB updates (done by liquibase itself)
ddl-auto: "none"
Expand All @@ -26,7 +31,6 @@ spring:
multipart:
max-file-size: "100MB"
max-request-size: "100MB"

logging:
pattern:
console: "%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable"
Expand All @@ -35,3 +39,4 @@ logging:
org.ebics: DEBUG
org.springframework.web: DEBUG
org.springframework.security: DEBUG

Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import org.springframework.context.annotation.Lazy
@Configuration
@Lazy
class ApiTestContext {
@MockkBean
lateinit var configuration: EbicsRestConfiguration

@Bean
fun bankConnectionService(): BankConnectionService = BankConnectionServiceTestImpl()
Expand All @@ -53,7 +51,7 @@ class ApiTestContext {
fun bankOperations(): BankOperations = BankOperationsTestImpl()

@Bean
fun ebicsBankAPI(): EbicsBankAPI = EbicsBankAPI(configuration, bankService(), versionSupportService(), bankOperations())
fun ebicsBankAPI(): EbicsBankAPI = EbicsBankAPI(bankService(), versionSupportService(), bankOperations())

@Bean
fun ebicsBankConnectionResource(): EbicsBankConnectionsResource = EbicsBankConnectionsResource(bankConnectionService(), healthStatusEnrichmentService())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import java.net.URI
@Configuration
@Lazy
class EbicsBankAPITestContext {
@MockkBean
private lateinit var configuration: EbicsRestConfiguration

@Bean
fun versionSupportService() = mockk<VersionSupportService>()
Expand Down Expand Up @@ -58,5 +56,5 @@ class EbicsBankAPITestContext {
fun bankOperations() = mockk<BankOperations>()

@Bean
fun ebicsBankApi() = EbicsBankAPI(configuration, bankService(), versionSupportService(), bankOperations())
fun ebicsBankApi() = EbicsBankAPI(bankService(), versionSupportService(), bankOperations())
}
1 change: 0 additions & 1 deletion ebics-web-ui/src/pages/BankConnectionInitWizz.vue
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,6 @@ import usePasswordAPI from 'components/password-api';
import useDialogs from 'components/dialogs';
import { copyToClipboard } from 'quasar';
import {
BankConnection,
UserCertificatesForImport
} from 'components/models/ebics-bank-connection';
Expand Down

0 comments on commit a557b20

Please sign in to comment.