Skip to content

Commit 6ec9af8

Browse files
authored
release: 1.9.5 (#351)
2 parents 96076e6 + 3847683 commit 6ec9af8

20 files changed

+365
-34
lines changed

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ jobs:
7474
"INTERNAL_SECRET=${{ secrets.INTERNAL_SECRET }}"
7575
"SLACK_TOKEN=${{ secrets.SLACK_TOKEN }}"
7676
"RELAY_APPROVE_TOKEN=${{ secrets.RELAY_APPROVE_TOKEN }}"
77+
"INTERNAL_AUTH_SECRET=${{ secrets.INTERNAL_AUTH_SECRET }}"
7778
7879
- name: build and push filebeat
7980
uses: docker/build-push-action@v4

deploy/api/Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ARG REDIS_PORT
99
ARG INTERNAL_SECRET
1010
ARG SLACK_TOKEN
1111
ARG RELAY_APPROVE_TOKEN
12+
ARG INTERNAL_AUTH_SECRET
1213

1314
ARG JAR_FILE=./*.jar
1415
COPY ${JAR_FILE} gitanimals-render.jar
@@ -21,7 +22,8 @@ ENV db_url=${DB_URL} \
2122
redis_port=${REDIS_PORT} \
2223
internal_secret=${INTERNAL_SECRET} \
2324
slack_token=${SLACK_TOKEN} \
24-
relay_approve_token=${RELAY_APPROVE_TOKEN}
25+
relay_approve_token=${RELAY_APPROVE_TOKEN} \
26+
internal_auth_secret=${INTERNAL_AUTH_SECRET}
2527

2628
ENTRYPOINT java -jar gitanimals-render.jar \
2729
--spring.datasource.url=${db_url} \
@@ -32,4 +34,5 @@ ENTRYPOINT java -jar gitanimals-render.jar \
3234
--github.token=${github_token} \
3335
--internal.secret=${internal_secret} \
3436
--slack.token=${slack_token} \
35-
--relay.approve.token=${relay_approve_token}
37+
--relay.approve.token=${relay_approve_token} \
38+
--internal.auth.secret=${internal_auth_secret}

src/main/kotlin/org/gitanimals/core/advice/GlobalExceptionHandler.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.gitanimals.core.advice
22

3+
import org.gitanimals.core.AuthorizationException
34
import org.gitanimals.core.ErrorResponse
45
import org.slf4j.LoggerFactory
56
import org.springframework.http.HttpStatus
@@ -52,4 +53,9 @@ class GlobalExceptionHandler {
5253
fun handleNoHandlerFoundException(exception: Exception): ErrorResponse {
5354
return ErrorResponse("CANNOT FOUND ANY RESOURCE.")
5455
}
56+
57+
@ExceptionHandler(AuthorizationException::class)
58+
@ResponseStatus(HttpStatus.UNAUTHORIZED)
59+
fun handleAuthorizationException(exception: AuthorizationException): ErrorResponse =
60+
ErrorResponse.from(exception)
5561
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.gitanimals.core.auth
2+
3+
import jakarta.servlet.http.HttpServletRequest
4+
import org.gitanimals.core.AUTHORIZATION_EXCEPTION
5+
import org.slf4j.LoggerFactory
6+
import org.springframework.beans.factory.annotation.Value
7+
import org.springframework.http.HttpHeaders
8+
import org.springframework.stereotype.Component
9+
import java.security.SecureRandom
10+
import java.util.*
11+
import javax.crypto.Cipher
12+
import javax.crypto.spec.GCMParameterSpec
13+
import javax.crypto.spec.SecretKeySpec
14+
15+
@Component
16+
class InternalAuth(
17+
private val httpServletRequest: HttpServletRequest,
18+
private val internalAuthClient: InternalAuthClient,
19+
@Value("\${internal.auth.secret}") private val internalAuthSecret: String,
20+
) {
21+
22+
private val logger = LoggerFactory.getLogger(this::class.simpleName)
23+
24+
private val secretKey by lazy {
25+
val decodedKey = Base64.getDecoder().decode(internalAuthSecret)
26+
SecretKeySpec(decodedKey, "AES")
27+
}
28+
29+
fun getUserId(throwOnFailure: () -> Unit = throwCannotGetUserId): Long {
30+
val userId = findUserId()
31+
32+
if (userId == null) {
33+
throwOnFailure.invoke()
34+
}
35+
36+
return userId ?: throw AUTHORIZATION_EXCEPTION
37+
}
38+
39+
fun findUserId(): Long? {
40+
val token: String? = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION)
41+
val iv: String? = httpServletRequest.getHeader(INTERNAL_AUTH_IV_KEY)
42+
val internalAuthSecret: String? = httpServletRequest.getHeader(INTERNAL_AUTH_SECRET_KEY)
43+
44+
var userId: Long? = runCatching {
45+
if (internalAuthSecret == null || iv == null) {
46+
return@runCatching null
47+
}
48+
return decrypt(
49+
iv = Base64.getDecoder().decode(iv),
50+
secret = Base64.getDecoder().decode(internalAuthSecret),
51+
)
52+
}.getOrElse {
53+
logger.warn("[InternalAuth] Fail to get userId by secret and iv", it)
54+
null
55+
}
56+
57+
if (token != null) {
58+
userId = runCatching {
59+
internalAuthClient.getUserByToken(token).id.toLong()
60+
}.getOrElse {
61+
logger.warn("[InternalAuth] Fail to get userId by token", it)
62+
null
63+
}
64+
}
65+
66+
return userId
67+
}
68+
69+
private fun decrypt(iv: ByteArray, secret: ByteArray): Long {
70+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
71+
val spec = GCMParameterSpec(128, iv)
72+
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
73+
74+
val decryptedBytes = cipher.doFinal(secret)
75+
return String(decryptedBytes, Charsets.UTF_8).toLong()
76+
}
77+
78+
fun encrypt(userId: Long): Encrypted {
79+
val iv = ByteArray(12)
80+
SecureRandom().nextBytes(iv)
81+
82+
val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
83+
val spec = GCMParameterSpec(128, iv)
84+
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec)
85+
86+
val cipherText = cipher.doFinal(userId.toString().toByteArray(Charsets.UTF_8))
87+
88+
return Encrypted(
89+
iv = iv,
90+
secret = cipherText,
91+
)
92+
}
93+
94+
companion object {
95+
const val INTERNAL_AUTH_IV_KEY = "Internal-Auth-Iv"
96+
const val INTERNAL_AUTH_SECRET_KEY = "Internal-Auth-Secret"
97+
98+
private val throwCannotGetUserId: () -> Unit = {
99+
throw AUTHORIZATION_EXCEPTION
100+
}
101+
}
102+
}
103+
104+
class Encrypted(
105+
val iv: ByteArray,
106+
val secret: ByteArray,
107+
)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.gitanimals.core.auth
2+
3+
import org.gitanimals.core.AuthorizationException
4+
import org.gitanimals.core.filter.MDCFilter
5+
import org.slf4j.MDC
6+
import org.springframework.context.annotation.Bean
7+
import org.springframework.context.annotation.Configuration
8+
import org.springframework.http.HttpHeaders
9+
import org.springframework.http.HttpStatus
10+
import org.springframework.http.client.ClientHttpResponse
11+
import org.springframework.web.bind.annotation.RequestHeader
12+
import org.springframework.web.client.ResponseErrorHandler
13+
import org.springframework.web.client.RestClient
14+
import org.springframework.web.client.support.RestClientAdapter
15+
import org.springframework.web.service.annotation.GetExchange
16+
import org.springframework.web.service.invoker.HttpServiceProxyFactory
17+
18+
fun interface InternalAuthClient {
19+
20+
@GetExchange("/users")
21+
fun getUserByToken(@RequestHeader(HttpHeaders.AUTHORIZATION) token: String): UserResponse
22+
23+
data class UserResponse(
24+
val id: String,
25+
)
26+
}
27+
28+
@Configuration
29+
class InternalAuthClientConfigurer {
30+
31+
@Bean
32+
fun internalAuthHttpClient(): InternalAuthClient {
33+
val restClient = RestClient
34+
.builder()
35+
.requestInterceptor { request, body, execution ->
36+
request.headers.add(MDCFilter.TRACE_ID, MDC.get(MDCFilter.TRACE_ID))
37+
execution.execute(request, body)
38+
}
39+
.defaultStatusHandler(HttpClientErrorHandler())
40+
.baseUrl("https://api.gitanimals.org")
41+
.build()
42+
43+
val httpServiceProxyFactory = HttpServiceProxyFactory
44+
.builderFor(RestClientAdapter.create(restClient))
45+
.build()
46+
47+
return httpServiceProxyFactory.createClient(InternalAuthClient::class.java)
48+
}
49+
50+
class HttpClientErrorHandler : ResponseErrorHandler {
51+
52+
override fun hasError(response: ClientHttpResponse): Boolean {
53+
return response.statusCode.isError
54+
}
55+
56+
override fun handleError(response: ClientHttpResponse) {
57+
val body = response.body.bufferedReader().use { it.readText() }
58+
when {
59+
response.statusCode.isSameCodeAs(HttpStatus.UNAUTHORIZED) ->
60+
throw AuthorizationException(body)
61+
62+
response.statusCode.is4xxClientError ->
63+
throw IllegalArgumentException(body)
64+
65+
response.statusCode.is5xxServerError ->
66+
throw IllegalStateException(body)
67+
}
68+
}
69+
}
70+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.gitanimals.core.auth
2+
3+
import org.gitanimals.core.filter.MDCFilter.Companion.USER_ID
4+
import org.slf4j.MDC
5+
import org.springframework.http.HttpRequest
6+
import org.springframework.http.client.ClientHttpRequestExecution
7+
import org.springframework.http.client.ClientHttpRequestInterceptor
8+
import org.springframework.http.client.ClientHttpResponse
9+
import org.springframework.stereotype.Component
10+
import java.util.*
11+
12+
@Component
13+
class InternalAuthRequestInterceptor(
14+
private val internalAuth: InternalAuth,
15+
) : ClientHttpRequestInterceptor {
16+
17+
override fun intercept(
18+
request: HttpRequest,
19+
body: ByteArray,
20+
execution: ClientHttpRequestExecution
21+
): ClientHttpResponse {
22+
val userId = runCatching {
23+
MDC.get(USER_ID).toLong()
24+
}.getOrNull()
25+
26+
if (userId != null) {
27+
val encrypt = internalAuth.encrypt(userId = userId)
28+
29+
request.headers.add(
30+
InternalAuth.INTERNAL_AUTH_SECRET_KEY,
31+
Base64.getEncoder().encodeToString(encrypt.secret),
32+
)
33+
request.headers.add(
34+
InternalAuth.INTERNAL_AUTH_IV_KEY,
35+
Base64.getEncoder().encodeToString(encrypt.iv),
36+
)
37+
}
38+
39+
return execution.execute(request, body)
40+
}
41+
}

src/main/kotlin/org/gitanimals/core/filter/MDCFilter.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import jakarta.servlet.FilterChain
44
import jakarta.servlet.http.HttpServletRequest
55
import jakarta.servlet.http.HttpServletResponse
66
import org.gitanimals.core.IdGenerator
7+
import org.gitanimals.core.auth.InternalAuth
78
import org.slf4j.LoggerFactory
89
import org.slf4j.MDC
910
import org.springframework.stereotype.Component
1011
import org.springframework.web.filter.OncePerRequestFilter
1112
import kotlin.system.measureTimeMillis
1213

1314
@Component
14-
class MDCFilter : OncePerRequestFilter() {
15+
class MDCFilter(
16+
private val internalAuth: InternalAuth,
17+
) : OncePerRequestFilter() {
1518

1619
override fun doFilterInternal(
1720
request: HttpServletRequest,
@@ -25,6 +28,7 @@ class MDCFilter : OncePerRequestFilter() {
2528

2629
MDC.put(TRACE_ID, traceId)
2730
MDC.put(PATH, request.requestURI)
31+
internalAuth.findUserId()?.let { MDC.put(USER_ID, it.toString()) }
2832

2933
val elapsedTime = measureTimeMillis {
3034
filterChain.doFilter(request, response)
@@ -38,10 +42,12 @@ class MDCFilter : OncePerRequestFilter() {
3842
MDC.remove(TRACE_ID)
3943
MDC.remove(ELAPSED_TIME)
4044
MDC.remove(PATH)
45+
MDC.remove(USER_ID)
4146
}
4247
}
4348

4449
companion object {
50+
const val USER_ID = "userId"
4551
const val TRACE_ID = "traceId"
4652
const val ELAPSED_TIME = "elapsedTime"
4753
const val PATH = "path"

src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
package org.gitanimals.guild.app
22

3+
import org.gitanimals.core.auth.InternalAuth
34
import org.gitanimals.guild.domain.GuildService
45
import org.springframework.stereotype.Service
56

67
@Service
78
class AcceptJoinGuildFacade(
8-
private val identityApi: IdentityApi,
9+
private val internalAuth: InternalAuth,
910
private val guildService: GuildService,
1011
) {
1112

12-
fun acceptJoin(token: String, guildId: Long, acceptUserId: Long) {
13-
val user = identityApi.getUserByToken(token)
13+
fun acceptJoin(guildId: Long, acceptUserId: Long) {
14+
val userId = internalAuth.getUserId()
1415

1516
guildService.acceptJoin(
16-
acceptorId = user.id.toLong(),
17+
acceptorId = userId,
1718
guildId = guildId,
1819
acceptUserId = acceptUserId,
1920
)

src/main/kotlin/org/gitanimals/guild/app/ChangeGuildFacade.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
package org.gitanimals.guild.app
22

3+
import org.gitanimals.core.auth.InternalAuth
34
import org.gitanimals.guild.domain.GuildService
45
import org.gitanimals.guild.domain.request.ChangeGuildRequest
56
import org.springframework.stereotype.Service
67

78
@Service
89
class ChangeGuildFacade(
9-
private val identityApi: IdentityApi,
10+
private val internalAuth: InternalAuth,
1011
private val guildService: GuildService,
1112
) {
1213

13-
fun changeGuild(token: String, guildId: Long, changeGuildRequest: ChangeGuildRequest) {
14+
fun changeGuild(guildId: Long, changeGuildRequest: ChangeGuildRequest) {
1415
changeGuildRequest.requireValidTitle()
1516

16-
val user = identityApi.getUserByToken(token)
17+
val userId = internalAuth.getUserId()
1718

1819
guildService.changeGuild(
19-
changeRequesterId = user.id.toLong(),
20+
changeRequesterId = userId,
2021
guildId = guildId,
2122
request = changeGuildRequest,
2223
)
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package org.gitanimals.guild.app
22

3+
import org.gitanimals.core.auth.InternalAuth
34
import org.gitanimals.guild.domain.GuildService
45
import org.springframework.stereotype.Service
56

67
@Service
78
class DenyJoinGuildFacade(
8-
private val identityApi: IdentityApi,
9+
private val internalAuth: InternalAuth,
910
private val guildService: GuildService,
1011
) {
1112

12-
fun denyJoin(token: String, guildId: Long, denyUserId: Long) {
13-
val user = identityApi.getUserByToken(token)
13+
fun denyJoin(guildId: Long, denyUserId: Long) {
14+
val userId = internalAuth.getUserId()
1415

15-
guildService.denyJoin(denierId = user.id.toLong(), guildId = guildId, denyUserId = denyUserId)
16+
guildService.denyJoin(denierId = userId, guildId = guildId, denyUserId = denyUserId)
1617
}
1718
}

0 commit comments

Comments
 (0)