Skip to content

Commit 245ef40

Browse files
authored
Created a client wrapper to make it easier to work with (#96)
Created a client wrapper to make it easier to work with - can be retrieved via jitpack Use Dispatchers IO as thread dispatcher for client Added authentication to gRpc server and client Added okhttp as gprc channel provider navikt/dagpenger#406
1 parent 8cacc38 commit 245ef40

File tree

10 files changed

+401
-17
lines changed

10 files changed

+401
-17
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Build and deploy dp-inntekt-grpc
2+
3+
on:
4+
push:
5+
paths:
6+
- 'dp-inntekt-grpc/**'
7+
8+
9+
jobs:
10+
11+
build:
12+
name: Build
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v1
17+
- name: Set up Java 12
18+
uses: actions/setup-java@v1
19+
with:
20+
java-version: '13.x'
21+
- name: Cache gradle dependencies
22+
uses: actions/cache@v1
23+
with:
24+
path: ~/.gradle/caches
25+
key: ${{ runner.os }}-gradle-${{ hashFiles('dp-inntekt-grpc/build.gradle.kts') }}-${{ hashFiles('buildSrc/src/main/kotlin/Constants.kt') }}
26+
restore-keys: |
27+
${{ runner.os }}-gradle-
28+
- name: Build with Gradle
29+
run: ./gradlew :dp-inntekt-grpc:build
30+
31+
32+
release:
33+
name: Create Release
34+
needs: build
35+
runs-on: ubuntu-latest
36+
if: github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, 'ci skip')
37+
steps:
38+
- name: Checkout code
39+
uses: actions/checkout@v1
40+
- name: Set release tag
41+
run: |
42+
export TAG_NAME="$(TZ="Europe/Oslo" date +%Y.%m.%d-%H.%M).$(echo $GITHUB_SHA | cut -c 1-12)"
43+
echo "::set-env name=RELEASE_TAG::$TAG_NAME"
44+
- name: Set changelog
45+
# (Escape newlines see https://github.com/actions/create-release/issues/25)
46+
run: |
47+
text="$(git --no-pager log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"%h %s")"
48+
text="${text//$'%'/%25}"
49+
text="${text//$'\n'/%0A}"
50+
text="${text//$'\r'/%0D}"
51+
echo "::set-env name=CHANGELOG::$text"
52+
- name: Create Release
53+
id: create_release
54+
uses: actions/create-release@latest
55+
env:
56+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57+
with:
58+
tag_name: ${{ env.RELEASE_TAG }}
59+
release_name: ${{ env.RELEASE_TAG }}
60+
body: |
61+
Changes in this Release
62+
${{ env.CHANGELOG }}
63+
draft: false
64+
prerelease: false

build.gradle.kts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ plugins {
88

99
val grpcVersion = "1.27.2"
1010

11-
repositories {
12-
jcenter()
13-
maven("https://jitpack.io")
14-
}
15-
1611
allprojects {
1712
group = "no.nav.dagpenger"
1813

@@ -73,6 +68,11 @@ allprojects {
7368
tasks.named("jar") {
7469
dependsOn("test")
7570
}
71+
72+
repositories {
73+
jcenter()
74+
maven("https://jitpack.io")
75+
}
7676
}
7777

7878
subprojects {

dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/InntektApi.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import org.slf4j.event.Level
6464

6565
private val LOGGER = KotlinLogging.logger {}
6666
private val sikkerLogg = KotlinLogging.logger("tjenestekall")
67-
val config = Configuration()
67+
private val config = Configuration()
6868

6969
fun main() = runBlocking {
7070

@@ -74,8 +74,7 @@ fun main() = runBlocking {
7474
.rateLimited(10, 1, TimeUnit.MINUTES)
7575
.build()
7676

77-
val apiKeyVerifier = ApiKeyVerifier(config.application.apiSecret)
78-
val allowedApiKeys = config.application.allowedApiKeys
77+
val authApiKeyVerifier = AuthApiKeyVerifier(ApiKeyVerifier(config.application.apiSecret), config.application.allowedApiKeys)
7978

8079
val dataSource = dataSourceFrom(config)
8180
val postgresInntektStore = PostgresInntektStore(dataSource)
@@ -86,7 +85,7 @@ fun main() = runBlocking {
8685
listen()
8786
}
8887

89-
val gRpcServer = InntektGrpcServer(port = 50051, inntektStore = postgresInntektStore)
88+
val gRpcServer = InntektGrpcServer(port = 50051, inntektStore = postgresInntektStore, apiKeyVerifier = authApiKeyVerifier)
9089

9190
launch {
9291
gRpcServer.start()
@@ -120,7 +119,7 @@ fun main() = runBlocking {
120119
postgresInntektStore,
121120
cachedInntektsGetter,
122121
oppslagClient,
123-
AuthApiKeyVerifier(apiKeyVerifier, allowedApiKeys),
122+
authApiKeyVerifier,
124123
jwkProvider,
125124
listOf(
126125
postgresInntektStore as HealthCheck,

dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/rpc/InntektGrpcServer.kt

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
11
package no.nav.dagpenger.inntekt.rpc
22

3+
import com.squareup.moshi.JsonAdapter
4+
import io.grpc.Metadata
35
import io.grpc.Server
46
import io.grpc.ServerBuilder
7+
import io.grpc.ServerCall
8+
import io.grpc.ServerCallHandler
9+
import io.grpc.ServerInterceptor
510
import io.grpc.Status
611
import io.grpc.StatusException
12+
import io.grpc.StatusRuntimeException
713
import mu.KotlinLogging
814
import no.nav.dagpenger.events.inntekt.v1.SpesifisertInntekt
15+
import no.nav.dagpenger.inntekt.AuthApiKeyVerifier
916
import no.nav.dagpenger.inntekt.db.IllegalInntektIdException
1017
import no.nav.dagpenger.inntekt.db.InntektNotFoundException
1118
import no.nav.dagpenger.inntekt.db.InntektStore
1219
import no.nav.dagpenger.inntekt.moshiInstance
1320

14-
internal class InntektGrpcServer(private val port: Int, inntektStore: InntektStore) {
15-
16-
companion object {
17-
private val logger = KotlinLogging.logger {}
18-
}
21+
private val logger = KotlinLogging.logger {}
22+
internal class InntektGrpcServer(
23+
private val port: Int,
24+
private val apiKeyVerifier: AuthApiKeyVerifier,
25+
private val inntektStore: InntektStore
26+
) {
27+
private val inntektGrpcApi = InntektGrpcApi(inntektStore).bindService()
1928

2029
private val server: Server = ServerBuilder
2130
.forPort(port)
22-
.addService(InntektGrpcApi(inntektStore))
31+
.addService(inntektGrpcApi)
32+
.intercept(ApiKeyServerInterceptor(apiKeyVerifier))
2333
.build()
2434

2535
fun start() {
@@ -43,9 +53,30 @@ internal class InntektGrpcServer(private val port: Int, inntektStore: InntektSto
4353
}
4454
}
4555

56+
internal class ApiKeyServerInterceptor(private val apiKeyVerifier: AuthApiKeyVerifier) : ServerInterceptor {
57+
58+
companion object {
59+
private val API_KEY_HEADER: Metadata.Key<String> =
60+
Metadata.Key.of("x-api-key", Metadata.ASCII_STRING_MARSHALLER)
61+
}
62+
63+
override fun <ReqT : Any?, RespT : Any?> interceptCall(
64+
call: ServerCall<ReqT, RespT>,
65+
headers: Metadata,
66+
next: ServerCallHandler<ReqT, RespT>
67+
): ServerCall.Listener<ReqT> {
68+
val authenticated = headers.get(API_KEY_HEADER)?.let { apiKeyVerifier.verify(it) } ?: false
69+
if (!authenticated) {
70+
logger.warn { "gRpc call not authenticated" }
71+
throw StatusRuntimeException(Status.UNAUTHENTICATED)
72+
}
73+
return next.startCall(call, headers)
74+
}
75+
}
76+
4677
internal class InntektGrpcApi(private val inntektStore: InntektStore) : SpesifisertInntektHenterGrpcKt.SpesifisertInntektHenterCoroutineImplBase() {
4778
companion object {
48-
val spesifisertInntektAdapter = moshiInstance.adapter(SpesifisertInntekt::class.java)
79+
val spesifisertInntektAdapter: JsonAdapter<SpesifisertInntekt> = moshiInstance.adapter(SpesifisertInntekt::class.java)
4980
}
5081

5182
override suspend fun hentSpesifisertInntektAsJson(request: InntektId): SpesifisertInntektAsJson {

dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/rpc/InntektGrpcApiTest.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package no.nav.dagpenger.inntekt.rpc
22

33
import de.huxhorn.sulky.ulid.ULID
4+
import io.grpc.Metadata
5+
import io.grpc.Metadata.ASCII_STRING_MARSHALLER
6+
import io.grpc.ServerCall
7+
import io.grpc.ServerCallHandler
48
import io.grpc.Status
9+
import io.grpc.Status.UNAUTHENTICATED
510
import io.grpc.StatusException
11+
import io.grpc.StatusRuntimeException
612
import io.kotest.matchers.shouldBe
713
import io.kotest.matchers.shouldNotBe
814
import io.mockk.every
915
import io.mockk.mockk
16+
import io.mockk.verify
1017
import java.time.LocalDateTime
1118
import java.time.YearMonth
1219
import kotlinx.coroutines.runBlocking
1320
import no.nav.dagpenger.events.inntekt.v1.Aktør
1421
import no.nav.dagpenger.events.inntekt.v1.AktørType
1522
import no.nav.dagpenger.events.inntekt.v1.SpesifisertInntekt
23+
import no.nav.dagpenger.inntekt.AuthApiKeyVerifier
1624
import no.nav.dagpenger.inntekt.db.InntektNotFoundException
1725
import no.nav.dagpenger.inntekt.db.InntektStore
1826
import no.nav.dagpenger.inntekt.moshiInstance
27+
import no.nav.dagpenger.ktor.auth.ApiKeyVerifier
1928
import org.junit.Test
2029
import org.junit.jupiter.api.assertThrows
2130

@@ -74,3 +83,37 @@ internal class InntektGrpcApiTest : GrpcTest() {
7483
spesifisertInntekt shouldBe spesifisertInntektResponse
7584
}
7685
}
86+
87+
internal class ApiKeyServerInterceptorTest {
88+
89+
@Test
90+
fun ` Should throw authenticated exception if no api key present `() {
91+
val verifier = AuthApiKeyVerifier(ApiKeyVerifier("secret"), listOf("client"))
92+
val apiKeyServerInterceptor = ApiKeyServerInterceptor(verifier)
93+
94+
val mockedServerCall = mockk<ServerCall<Any, Any>>(relaxed = true)
95+
val headers = Metadata()
96+
val mockedServerCallHandler = mockk<ServerCallHandler<Any, Any>>(relaxed = true)
97+
98+
val exc = assertThrows<StatusRuntimeException> { apiKeyServerInterceptor.interceptCall(mockedServerCall, headers, mockedServerCallHandler) }
99+
exc.status.code shouldBe UNAUTHENTICATED.code
100+
101+
verify(exactly = 0) { mockedServerCallHandler.startCall(mockedServerCall, headers) }
102+
}
103+
104+
@Test
105+
fun ` Should forward authenticated calls `() {
106+
val keyVerifier = ApiKeyVerifier("secret")
107+
val apiVerifier = AuthApiKeyVerifier(keyVerifier, listOf("client"))
108+
val apiKeyServerInterceptor = ApiKeyServerInterceptor(apiVerifier)
109+
110+
val mockedServerCall = mockk<ServerCall<Any, Any>>(relaxed = true)
111+
val headers = Metadata()
112+
headers.put(Metadata.Key.of("x-api-key", ASCII_STRING_MARSHALLER), keyVerifier.generate("client"))
113+
val mockedServerCallHandler = mockk<ServerCallHandler<Any, Any>>(relaxed = true)
114+
115+
apiKeyServerInterceptor.interceptCall(mockedServerCall, headers, mockedServerCallHandler)
116+
117+
verify(exactly = 1) { mockedServerCallHandler.startCall(mockedServerCall, headers) }
118+
}
119+
}

dp-inntekt-grpc/build.gradle.kts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ val protobufGradleVersion = "0.8.12"
88
plugins {
99
kotlin("jvm")
1010
id("com.google.protobuf") version "0.8.12"
11+
`java-library`
12+
`maven-publish`
1113
}
1214

1315
apply(plugin = "com.google.protobuf")
1416

17+
group = "com.github.navikt"
18+
version = "1.0-SNAPSHOT"
19+
1520
// To get intellij to make sense of generated sources
1621
java {
1722
val mainJavaSourceSet: SourceDirectorySet = sourceSets.getByName("main").java
@@ -21,19 +26,38 @@ java {
2126

2227
dependencies {
2328
implementation(kotlin("stdlib-jdk8"))
29+
2430
// Grpc and Protobuf
2531
protobuf(files("src/proto/"))
2632
implementation("com.google.protobuf:protobuf-gradle-plugin:$protobufGradleVersion")
2733
api("io.grpc:grpc-api:$grpcVersion")
2834
api("io.grpc:grpc-protobuf:$grpcVersion")
2935
api("io.grpc:grpc-stub:$grpcVersion")
3036
api("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion")
37+
implementation("io.grpc:grpc-okhttp:$grpcVersion")
3138

3239
if (JavaVersion.current().isJava9Compatible) {
3340
// Workaround for @javax.annotation.Generated
3441
// see: https://github.com/grpc/grpc-java/issues/3633
3542
compileOnly("javax.annotation:javax.annotation-api:1.3.2")
3643
}
44+
45+
//
46+
implementation(Kotlin.Coroutines.module("core"))
47+
48+
// events
49+
implementation(Dagpenger.Events)
50+
implementation(Moshi.moshi)
51+
52+
// api key verifier
53+
implementation(Dagpenger.Biblioteker.ktorUtils)
54+
55+
// test
56+
testImplementation("io.grpc:grpc-testing:$grpcVersion")
57+
testImplementation(Junit5.api)
58+
testRuntimeOnly(Junit5.engine)
59+
testRuntimeOnly(Junit5.vintageEngine)
60+
testImplementation(KoTest.assertions)
3761
}
3862

3963
// Could not resolve all files for configuration ':protomodule:compileProtoPath' bug

0 commit comments

Comments
 (0)