Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import java.security.Principal
class APIKeyController(private val apiKeyService: APIKeyServiceImpl) {

@GetMapping
suspend fun getKeys(principal: Principal): List<APIKeyResponse> {
fun getKeys(principal: Principal): List<APIKeyResponse> {
return apiKeyService.getKeysByUserId(principal.name)
.map { APIKeyResponse(it.label, it.expirationTime, it.allowedIPs, it.key, it.isEnabled) }
}

@PostMapping
suspend fun create(
fun create(
@RequestBody request: CreateAPIKeyRequest,
@CurrentSecurityContext securityContext: SecurityContext
): Any {
Expand All @@ -40,17 +40,17 @@ class APIKeyController(private val apiKeyService: APIKeyServiceImpl) {
}

@PutMapping("/{key}/enable")
suspend fun enableKey(principal: Principal, @PathVariable key: String) {
fun enableKey(principal: Principal, @PathVariable key: String) {
apiKeyService.changeKeyState(principal.name, key, true)
}

@PutMapping("/{key}/disable")
suspend fun disableKey(principal: Principal, @PathVariable key: String) {
fun disableKey(principal: Principal, @PathVariable key: String) {
apiKeyService.changeKeyState(principal.name, key, false)
}

@DeleteMapping("/{key}")
suspend fun deleteKey(principal: Principal, @PathVariable key: String) {
fun deleteKey(principal: Principal, @PathVariable key: String) {
apiKeyService.deleteKey(principal.name, key)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,54 @@ package co.nilin.opex.api.app.interceptor

import co.nilin.opex.api.app.service.APIKeyServiceImpl
import co.nilin.opex.api.core.spi.APIKeyFilter
import kotlinx.coroutines.runBlocking
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletRequestWrapper
import jakarta.servlet.http.HttpServletResponse
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import org.springframework.web.filter.OncePerRequestFilter
import java.util.*

@Component
class APIKeyFilterImpl(private val apiKeyService: APIKeyServiceImpl) : APIKeyFilter, WebFilter {
class APIKeyFilterImpl(private val apiKeyService: APIKeyServiceImpl) : APIKeyFilter, OncePerRequestFilter() {

override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val request = exchange.request
val key = request.headers["X-API-KEY"]
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain
) {
val key = request.getHeader("X-API-KEY")
if (!key.isNullOrEmpty()) {
val secret = request.headers["X-API-SECRET"]
if (secret.isNullOrEmpty())
return chain.filter(exchange)
val secret = request.getHeader("X-API-KEY")
if (secret.isNullOrEmpty()) {
chain.doFilter(request, response)
return
}

val apiKey = runBlocking { apiKeyService.getAPIKey(key[0], secret[0]) }
val apiKey = apiKeyService.getAPIKey(key, secret)
if (apiKey != null && apiKey.isEnabled && apiKey.accessToken != null && !apiKey.isExpired) {
val req = exchange.request.mutate()
.header("Authorization", "Bearer ${apiKey.accessToken}")
.build()
return chain.filter(exchange.mutate().request(req).build())
val wrappedReq = RequestWrapper(request)
wrappedReq.addHeader("Authorization", "Bearer ${apiKey.accessToken}")
chain.doFilter(wrappedReq, response)
return
}
}
return chain.filter(exchange)
chain.doFilter(request, response)
}

}

class RequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {

private val customHeaders = hashMapOf<String, String>()

fun addHeader(key: String, value: String) {
customHeaders[key] = value
}

override fun getHeaderNames(): Enumeration<String> {
val names = HashSet(Collections.list(super.getHeaderNames()))
names.addAll(customHeaders.keys)
return Collections.enumeration(names)
}
}
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
package co.nilin.opex.api.app.proxy

import co.nilin.opex.api.app.data.AccessTokenResponse
import kotlinx.coroutines.reactor.awaitSingle
import co.nilin.opex.common.OpexError
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.client.RestTemplate
import org.springframework.web.client.exchange
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToMono

@Component
class AuthProxy(
@Value("\${app.auth.token-url}")
private val tokenUrl: String
private val tokenUrl: String,
private val restTemplate: RestTemplate
) {

private val logger = LoggerFactory.getLogger(AuthProxy::class.java)
private val client = WebClient.create()

suspend fun exchangeToken(clientSecret: String, token: String): AccessTokenResponse {
fun exchangeToken(clientSecret: String, token: String): AccessTokenResponse {
val body = BodyInserters.fromFormData("client_id", "opex-api-key")
.with("client_secret", clientSecret)
.with("subject_token", token)
.with("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
.with("scope", "offline_access")

logger.info("Request token exchange for user")
return client.post()
.uri(tokenUrl)
.accept(MediaType.APPLICATION_JSON)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToMono<AccessTokenResponse>()
.awaitSingle()

val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_FORM_URLENCODED
accept = listOf(MediaType.APPLICATION_JSON)
}
val response = restTemplate.exchange<AccessTokenResponse>(tokenUrl, HttpMethod.POST, HttpEntity(body, headers))
return response.body ?: throw OpexError.InternalServerError.exception()
}

suspend fun refreshToken(clientSecret: String, refreshToken: String): AccessTokenResponse {
fun refreshToken(clientSecret: String, refreshToken: String): AccessTokenResponse {
val body = BodyInserters.fromFormData("client_id", "opex-api-key")
.with("client_secret", clientSecret)
.with("refresh_token", refreshToken)
.with("grant_type", "refresh_token")

logger.info("Refreshing token")
return client.post()
.uri(tokenUrl)
.accept(MediaType.APPLICATION_JSON)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToMono<AccessTokenResponse>()
.awaitSingle()

val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_FORM_URLENCODED
accept = listOf(MediaType.APPLICATION_JSON)
}
val response = restTemplate.exchange<AccessTokenResponse>(tokenUrl, HttpMethod.POST, HttpEntity(body, headers))
return response.body ?: throw OpexError.InternalServerError.exception()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import co.nilin.opex.api.core.spi.APIKeyService
import co.nilin.opex.api.ports.postgres.dao.APIKeyRepository
import co.nilin.opex.api.ports.postgres.model.APIKeyModel
import co.nilin.opex.common.OpexError
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.Cache
Expand All @@ -37,14 +31,14 @@ class APIKeyServiceImpl(

private val logger = LoggerFactory.getLogger(APIKeyServiceImpl::class.java)

override suspend fun createAPIKey(
override fun createAPIKey(
userId: String,
label: String,
expirationTime: LocalDateTime?,
allowedIPs: String?,
currentToken: String
): Pair<String, APIKey> {
if (apiKeyRepository.countByUserId(userId).awaitFirstOrElse { 0 } >= 10)
if ((apiKeyRepository.countByUserId(userId) ?: 0) >= 10)
throw OpexError.APIKeyLimitReached.exception()

val secret = generateSecret()
Expand All @@ -60,7 +54,7 @@ class APIKeyServiceImpl(
allowedIPs,
tokenExpiration(tokenResponse.expires_in)
)
).awaitSingle()
)

return Pair(
secret,
Expand All @@ -70,13 +64,12 @@ class APIKeyServiceImpl(
)
}

override suspend fun getAPIKey(key: String, secret: String): APIKey? = coroutineScope {
val apiKey = getFromCache(key)
?: apiKeyRepository.findByKey(key).awaitSingleOrNull()?.apply { putCache(this) }
override fun getAPIKey(key: String, secret: String): APIKey? {
val apiKey = getFromCache(key) ?: apiKeyRepository.findByKey(key)?.apply { putCache(this) }

with(apiKey) {
return with(apiKey) {
if (this != null) {
launch { checkupAPIKey(this@with, secret) }
checkupAPIKey(this@with, secret)
APIKey(
userId,
label,
Expand All @@ -92,8 +85,8 @@ class APIKeyServiceImpl(
}
}

override suspend fun getKeysByUserId(userId: String): List<APIKey> {
return apiKeyRepository.findAllByUserId(userId).collectList().awaitFirstOrElse { emptyList() }
override fun getKeysByUserId(userId: String): List<APIKey> {
return apiKeyRepository.findAllByUserId(userId)
.map {
APIKey(
it.userId,
Expand All @@ -108,22 +101,22 @@ class APIKeyServiceImpl(
}
}

override suspend fun changeKeyState(userId: String, key: String, isEnabled: Boolean) {
val apiKey = apiKeyRepository.findByKey(key).awaitSingleOrNull() ?: throw OpexError.NotFound.exception()
override fun changeKeyState(userId: String, key: String, isEnabled: Boolean) {
val apiKey = apiKeyRepository.findByKey(key) ?: throw OpexError.NotFound.exception()
if (apiKey.userId != userId)
throw OpexError.Forbidden.exception()
apiKey.isEnabled = isEnabled
apiKeyRepository.save(apiKey).awaitSingle()
apiKeyRepository.save(apiKey)
}

override suspend fun deleteKey(userId: String, key: String) {
val apiKey = apiKeyRepository.findByKey(key).awaitSingleOrNull() ?: throw OpexError.NotFound.exception()
override fun deleteKey(userId: String, key: String) {
val apiKey = apiKeyRepository.findByKey(key) ?: throw OpexError.NotFound.exception()
if (apiKey.userId != userId)
throw OpexError.Forbidden.exception()
apiKeyRepository.delete(apiKey).awaitFirstOrNull()
apiKeyRepository.delete(apiKey)
}

private suspend fun checkupAPIKey(apiKey: APIKeyModel, secret: String) {
private fun checkupAPIKey(apiKey: APIKeyModel, secret: String) {
if (apiKey.isExpired || !apiKey.isEnabled)
return

Expand All @@ -132,7 +125,7 @@ class APIKeyServiceImpl(
if (apiKey.expirationTime?.isBefore(now) == true) {
logger.info("Expiring api key ${apiKey.key}")
apiKey.isExpired = true
apiKeyRepository.save(apiKey).awaitSingle().apply { updateCache(this) }
apiKeyRepository.save(apiKey).apply { updateCache(this) }
logger.info("API key ${apiKey.key} is expired")
return
}
Expand All @@ -144,7 +137,7 @@ class APIKeyServiceImpl(
accessToken = encryptAES(response.access_token, secret)
tokenExpirationTime = tokenExpiration(response.expires_in)
}
apiKeyRepository.save(apiKey).awaitSingle().apply { updateCache(this) }
apiKeyRepository.save(apiKey).apply { updateCache(this) }
logger.info("API key ${apiKey.key} token refreshed")
}
} catch (e: Exception) {
Expand Down
9 changes: 6 additions & 3 deletions api/api-app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ spring:
main:
allow-bean-definition-overriding: true
allow-circular-references: true
r2dbc:
url: r2dbc:postgresql://${DB_IP_PORT:localhost}/opex
datasource:
url: jdbc:postgresql://${DB_IP_PORT:localhost}/opex
username: ${dbusername:opex}
password: ${dbpassword:hiopex}
initialization-mode: always
driver-class-name: org.postgresql.Driver
sql:
init:
mode: always
cloud:
bootstrap:
enabled: true
Expand Down
8 changes: 0 additions & 8 deletions api/api-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,6 @@
<groupId>io.projectreactor.kotlin</groupId>
<artifactId>reactor-kotlin-extensions</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import java.time.LocalDateTime

interface APIKeyService {

suspend fun createAPIKey(
fun createAPIKey(
userId: String,
label: String,
expirationTime: LocalDateTime?,
allowedIPs: String?,
currentToken: String
): Pair<String, APIKey>

suspend fun getAPIKey(key: String, secret: String): APIKey?
fun getAPIKey(key: String, secret: String): APIKey?

suspend fun getKeysByUserId(userId: String): List<APIKey>
fun getKeysByUserId(userId: String): List<APIKey>

suspend fun changeKeyState(userId: String, key: String, isEnabled: Boolean)
fun changeKeyState(userId: String, key: String, isEnabled: Boolean)

suspend fun deleteKey(userId: String, key: String)
fun deleteKey(userId: String, key: String)

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import co.nilin.opex.api.core.inout.PairConfigResponse

interface AccountantProxy {

suspend fun getPairConfigs(): List<PairConfigResponse>
fun getPairConfigs(): List<PairConfigResponse>

suspend fun getFeeConfigs(): List<PairFeeResponse>
fun getFeeConfigs(): List<PairFeeResponse>

suspend fun getFeeConfig(symbol: String): PairFeeResponse
fun getFeeConfig(symbol: String): PairFeeResponse

}
Loading
Loading