Skip to content

Commit 5886c00

Browse files
feat: JWT annotations (#1141)
1 parent e9d6bb3 commit 5886c00

File tree

14 files changed

+303
-21
lines changed

14 files changed

+303
-21
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2021 Lightbend Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package kalix.springsdk.annotations;
18+
19+
import java.lang.annotation.*;
20+
21+
@Target(ElementType.METHOD)
22+
@Retention(RetentionPolicy.RUNTIME)
23+
@Documented
24+
public @interface JWT {
25+
26+
enum JwtMethodMode {
27+
// No validation.
28+
UNSPECIFIED,
29+
// Validate the bearer token.
30+
BEARER_TOKEN,
31+
// Validate/sign a token field in the message against the message fields.
32+
//
33+
// If present, the message must have a token annotated field or the message itself must have
34+
// validate_bearer_token
35+
// set to true.
36+
MESSAGE
37+
}
38+
39+
JwtMethodMode[] validate();
40+
41+
JwtMethodMode[] sign();
42+
// If set, then the token extracted from the bearer token must have this issuer.
43+
//
44+
// This can be used in combination with the issuer field of configuration for JWT secrets, if
45+
// there is at least one
46+
// secret that has this issuer set, then only those secrets with that issuer set will be used
47+
// for validating or
48+
// signing this token, so you can be sure that the token did come from a particular issuer.
49+
String[] bearerTokenIssuer();
50+
}

sdk/spring-sdk/src/main/scala/kalix/springsdk/impl/ActionDescriptorFactory.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import kalix.springsdk.impl.ComponentDescriptorFactory.eventingInForTopic
2121
import kalix.springsdk.impl.ComponentDescriptorFactory.eventingInForValueEntity
2222
import kalix.springsdk.impl.ComponentDescriptorFactory.eventingOutForTopic
2323
import kalix.springsdk.impl.ComponentDescriptorFactory.hasEventSourcedEntitySubscription
24+
import kalix.springsdk.impl.ComponentDescriptorFactory.hasJwtMethodOptions
2425
import kalix.springsdk.impl.ComponentDescriptorFactory.hasTopicPublication
2526
import kalix.springsdk.impl.ComponentDescriptorFactory.hasTopicSubscription
2627
import kalix.springsdk.impl.ComponentDescriptorFactory.hasValueEntitySubscription
28+
import kalix.springsdk.impl.ComponentDescriptorFactory.jwtMethodOptions
2729
import kalix.springsdk.impl.ComponentDescriptorFactory.validateRestMethod
2830
import kalix.springsdk.impl.reflection.CombinedSubscriptionServiceMethod
2931
import kalix.springsdk.impl.reflection.KalixMethod
@@ -39,7 +41,7 @@ private[impl] object ActionDescriptorFactory extends ComponentDescriptorFactory
3941
val springAnnotatedMethods =
4042
RestServiceIntrospector.inspectService(component).methods.map { serviceMethod =>
4143
validateRestMethod(serviceMethod.javaMethod)
42-
KalixMethod(serviceMethod)
44+
KalixMethod(serviceMethod).withKalixOptions(buildJWTOptions(serviceMethod.javaMethod))
4345
}
4446

4547
//TODO make sure no subscription should be exposed via REST.
@@ -121,19 +123,19 @@ private[impl] object ActionDescriptorFactory extends ComponentDescriptorFactory
121123
val serviceName = nameGenerator.getName(component.getSimpleName)
122124

123125
def filterAndAddKalixOptions(to: Seq[KalixMethod], from: Seq[KalixMethod]): Seq[KalixMethod] = {
124-
val common = to.flatMap(toAdd =>
126+
val inCommon = to.flatMap(toAdd =>
125127
from
126128
.filter { addingFrom =>
127129
addingFrom.serviceMethod.methodName.equals(toAdd.serviceMethod.methodName)
128130
}
129131
.map(addingFrom => toAdd.withKalixOptions(addingFrom.methodOptions)))
130-
val notInCommon = to
132+
val unique = to
131133
.filter { toAdd =>
132134
!from.exists { addingFrom =>
133135
addingFrom.serviceMethod.methodName.equals(toAdd.serviceMethod.methodName)
134136
}
135137
}
136-
common ++ notInCommon
138+
inCommon ++ unique
137139
}
138140

139141
def removeDuplicates(springMethods: Seq[KalixMethod], pubSubMethods: Seq[KalixMethod]): Seq[KalixMethod] = {

sdk/spring-sdk/src/main/scala/kalix/springsdk/impl/ComponentDescriptor.scala

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,8 @@ private[impl] object ComponentDescriptor {
103103
kalixMethod.serviceMethod.streamIn,
104104
kalixMethod.serviceMethod.streamOut)
105105

106-
val methodOptions =
107-
kalixMethod.methodOptions.foldLeft(MethodOptions.newBuilder()) { (optionsBuilder, options) =>
108-
optionsBuilder.setExtension(kalix.Annotations.method, options)
109-
}
106+
val methodOptions = MethodOptions.newBuilder()
107+
kalixMethod.methodOptions.foreach(option => methodOptions.setExtension(kalix.Annotations.method, option))
110108

111109
methodOptions.setExtension(AnnotationsProto.http, httpRuleBuilder.build())
112110

sdk/spring-sdk/src/main/scala/kalix/springsdk/impl/ComponentDescriptorFactory.scala

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616

1717
package kalix.springsdk.impl
1818

19+
import kalix.MethodOptions
20+
import kalix.springsdk.annotations.JWT
1921
import kalix.springsdk.annotations.Table
2022
import kalix.springsdk.annotations.{ Entity, Publish, Subscribe }
23+
import kalix.springsdk.impl.ComponentDescriptorFactory.hasJwtMethodOptions
24+
import kalix.springsdk.impl.ComponentDescriptorFactory.jwtMethodOptions
2125
import kalix.springsdk.impl.reflection._
22-
import kalix.{ EventDestination, EventSource, Eventing }
26+
import kalix.{ EventDestination, EventSource, Eventing, JwtMethodOptions }
2327

2428
import java.lang.reflect.{ Method, Modifier }
2529

@@ -41,6 +45,10 @@ private[impl] object ComponentDescriptorFactory {
4145
Modifier.isPublic(javaMethod.getModifiers) &&
4246
javaMethod.getAnnotation(classOf[Publish.Topic]) != null
4347

48+
def hasJwtMethodOptions(javaMehod: Method): Boolean =
49+
Modifier.isPublic(javaMehod.getModifiers) &&
50+
javaMehod.getAnnotation(classOf[JWT]) != null
51+
4452
def findEventSourcedEntityType(javaMethod: Method): String = {
4553
val ann = javaMethod.getAnnotation(classOf[Subscribe.EventSourcedEntity])
4654
val entityClass = ann.value()
@@ -74,6 +82,19 @@ private[impl] object ComponentDescriptorFactory {
7482
ann.value()
7583
}
7684

85+
def jwtMethodOptions(javaMethod: Method): JwtMethodOptions = {
86+
val ann = javaMethod.getAnnotation(classOf[JWT])
87+
val jwt = JwtMethodOptions.newBuilder()
88+
ann
89+
.validate()
90+
.map(springValidate => jwt.addValidate(JwtMethodOptions.JwtMethodMode.forNumber(springValidate.ordinal())))
91+
ann
92+
.sign()
93+
.map(springSign => jwt.addSign(JwtMethodOptions.JwtMethodMode.forNumber(springSign.ordinal())))
94+
ann.bearerTokenIssuer().map(jwt.addBearerTokenIssuer)
95+
jwt.build()
96+
}
97+
7798
def eventingInForValueEntity(javaMethod: Method): Eventing = {
7899
val entityType = findValueEntityType(javaMethod)
79100
val eventSource = EventSource.newBuilder().setValueEntity(entityType).build()
@@ -140,6 +161,7 @@ private[impl] trait ComponentDescriptorFactory {
140161
//Assuming there is only one eventing.in annotation per method, therefore head is as good as any other
141162
withEventSourcedIn.groupBy(m => m.methodOptions.head.getEventing.getIn.getEventSourcedEntity)
142163
}
164+
143165
groupByES(subscriptions).collect {
144166
case (eventSourcedEntity, kMethods) if kMethods.size > 1 =>
145167
val typeUrl2Method: Seq[TypeUrl2Method] = kMethods.map { k =>
@@ -161,6 +183,12 @@ private[impl] trait ComponentDescriptorFactory {
161183
kMethod
162184
}.toSeq
163185
}
186+
187+
private[impl] def buildJWTOptions(method: Method): Option[MethodOptions] = {
188+
Option.when(hasJwtMethodOptions(method)) {
189+
kalix.MethodOptions.newBuilder().setJwt(jwtMethodOptions(method)).build()
190+
}
191+
}
164192
}
165193

166194
/**

sdk/spring-sdk/src/main/scala/kalix/springsdk/impl/EntityDescriptorFactory.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ private[impl] object EntityDescriptorFactory extends ComponentDescriptorFactory
2828

2929
val kalixMethods =
3030
RestServiceIntrospector.inspectService(component).methods.map { restMethod =>
31-
KalixMethod(restMethod, entityKeys = entityKeys)
31+
KalixMethod(restMethod, entityKeys = entityKeys).withKalixOptions(buildJWTOptions(restMethod.javaMethod))
3232
}
3333

3434
val serviceName = nameGenerator.getName(component.getSimpleName)

sdk/spring-sdk/src/main/scala/kalix/springsdk/impl/ViewDescriptorFactory.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package kalix.springsdk.impl
1818

1919
import java.lang.reflect.Method
20-
20+
import java.lang.reflect.ParameterizedType
2121
import kalix.MethodOptions
2222
import kalix.springsdk.annotations.Query
2323
import kalix.springsdk.annotations.Subscribe
@@ -31,8 +31,6 @@ import kalix.springsdk.impl.reflection.RestServiceIntrospector
3131
import kalix.springsdk.impl.reflection.SpringRestServiceMethod
3232
import kalix.springsdk.impl.reflection.ReflectionUtils
3333
import kalix.springsdk.impl.reflection.RestServiceIntrospector.BodyParameter
34-
import java.lang.reflect.ParameterizedType
35-
3634
import kalix.springsdk.impl.ComponentDescriptorFactory.eventingInForEventSourcedEntity
3735
import kalix.springsdk.impl.ComponentDescriptorFactory.hasEventSourcedEntitySubscription
3836
import kalix.springsdk.impl.reflection.SubscriptionServiceMethod
@@ -147,7 +145,8 @@ private[impl] object ViewDescriptorFactory extends ComponentDescriptorFactory {
147145
// since it is a query, we don't actually ever want to handle any request in the SDK
148146
// the proxy does the work for us, mark the method as non-callable
149147
(
150-
KalixMethod(annotatedMethod.copy(callable = false), methodOptions = Seq(methodOptions)),
148+
KalixMethod(annotatedMethod.copy(callable = false), methodOptions = Some(methodOptions))
149+
.withKalixOptions(buildJWTOptions(annotatedMethod.javaMethod)),
151150
queryInputSchemaDescriptor,
152151
queryOutputSchemaDescriptor)
153152
}
@@ -279,7 +278,7 @@ private[impl] object ViewDescriptorFactory extends ComponentDescriptorFactory {
279278

280279
KalixMethod(SubscriptionServiceMethod(method, method.getParameterCount - 1))
281280
.withKalixOptions(methodOptionsBuilder.build())
282-
281+
.withKalixOptions(buildJWTOptions(method))
283282
}
284283
.toSeq
285284
}

sdk/spring-sdk/src/main/scala/kalix/springsdk/impl/reflection/KalixMethod.scala

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,38 @@ case class SpringRestServiceMethod(
208208

209209
case class KalixMethod(
210210
serviceMethod: ServiceMethod,
211-
methodOptions: Seq[kalix.MethodOptions] = Seq.empty,
211+
methodOptions: Option[kalix.MethodOptions] = None,
212212
entityKeys: Seq[String] = Seq.empty) {
213213

214+
/**
215+
* This method merges the new method options with the existing ones. In case of collision the 'opts' are kept
216+
* @param opts
217+
* @return
218+
*/
214219
def withKalixOptions(opts: kalix.MethodOptions): KalixMethod =
215-
copy(methodOptions = methodOptions :+ opts)
220+
copy(methodOptions = Some(mergeKalixOptions(methodOptions, opts)))
221+
222+
/**
223+
* This method merges the new method options with the existing ones. In case of collision the 'opts' are kept
224+
* @param opts
225+
* @return
226+
*/
227+
def withKalixOptions(opts: Option[kalix.MethodOptions]): KalixMethod =
228+
opts match {
229+
case Some(methodOptions) => withKalixOptions(methodOptions)
230+
case None => this
231+
}
216232

217-
def withKalixOptions(opts: Seq[kalix.MethodOptions]): KalixMethod =
218-
copy(methodOptions = methodOptions ++ opts)
233+
private[impl] def mergeKalixOptions(
234+
source: Option[kalix.MethodOptions],
235+
addOn: kalix.MethodOptions): kalix.MethodOptions = {
236+
val builder = source match {
237+
case Some(src) => src.toBuilder
238+
case None => kalix.MethodOptions.newBuilder()
239+
}
240+
builder.mergeFrom(addOn)
241+
builder.build()
242+
}
219243
}
220244

221245
trait ExtractorCreator {

sdk/spring-sdk/src/test/java/kalix/springsdk/testmodels/action/ActionsTestModels.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
package kalix.springsdk.testmodels.action;
1818

19-
import akka.NotUsed;
20-
import akka.stream.javadsl.Source;
19+
import kalix.JwtMethodOptions.JwtMethodMode;
2120
import kalix.javasdk.action.Action;
21+
import kalix.springsdk.annotations.JWT;
2222
import kalix.springsdk.testmodels.Message;
2323
import org.springframework.web.bind.annotation.*;
2424
import reactor.core.publisher.Flux;
@@ -54,6 +54,17 @@ public Action.Effect<Message> message(@RequestBody Message msg) {
5454
}
5555
}
5656

57+
public static class PostWithoutParamWithJWT extends Action {
58+
@PostMapping("/message")
59+
@JWT(
60+
validate = JWT.JwtMethodMode.BEARER_TOKEN,
61+
sign = JWT.JwtMethodMode.MESSAGE,
62+
bearerTokenIssuer = {"a", "b"})
63+
public Action.Effect<Message> message(@RequestBody Message msg) {
64+
return effects().reply(msg);
65+
}
66+
}
67+
5768
public static class PostWithOneParam extends Action {
5869
@PostMapping("/message/{one}")
5970
public Action.Effect<Message> message(@PathVariable String one, @RequestBody Message msg) {

sdk/spring-sdk/src/test/java/kalix/springsdk/testmodels/eventsourcedentity/EventSourcedEntitiesTestModels.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package kalix.springsdk.testmodels.eventsourcedentity;
1818

19+
import kalix.JwtMethodOptions;
1920
import kalix.javasdk.eventsourcedentity.EventSourcedEntity;
2021
import kalix.springsdk.annotations.Entity;
2122
import kalix.springsdk.annotations.EventHandler;
23+
import kalix.springsdk.annotations.JWT;
24+
import kalix.springsdk.annotations.Subscribe;
25+
import kalix.springsdk.testmodels.valueentity.UserEntity;
2226
import org.springframework.web.bind.annotation.*;
2327

2428
public class EventSourcedEntitiesTestModels {
@@ -74,6 +78,29 @@ private Integer privateMethodSimilarSignature(Integer event) {
7478
}
7579
}
7680

81+
@Entity(entityKey = "id", entityType = "counter")
82+
@RequestMapping("/eventsourced/{id}")
83+
public static class WellAnnotatedESEntityWithJWT extends EventSourcedEntity<Integer> {
84+
85+
@GetMapping("/int/{number}")
86+
@JWT(
87+
validate = JWT.JwtMethodMode.BEARER_TOKEN,
88+
sign = JWT.JwtMethodMode.MESSAGE,
89+
bearerTokenIssuer = {"a", "b"})
90+
public Integer getInteger(@PathVariable Integer number) {
91+
return number;
92+
}
93+
94+
@PostMapping("/changeInt/{number}")
95+
@JWT(
96+
validate = JWT.JwtMethodMode.BEARER_TOKEN,
97+
sign = JWT.JwtMethodMode.MESSAGE,
98+
bearerTokenIssuer = {"a", "b"})
99+
public Integer changeInteger(@PathVariable Integer number) {
100+
return number;
101+
}
102+
}
103+
77104
@Entity(entityKey = "id", entityType = "counter")
78105
public static class ErrorDuplicatedEventsEntity extends EventSourcedEntity<Integer> {
79106

sdk/spring-sdk/src/test/java/kalix/springsdk/testmodels/view/ViewTestModels.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package kalix.springsdk.testmodels.view;
1818

1919
import kalix.javasdk.view.View;
20+
import kalix.springsdk.annotations.JWT;
2021
import kalix.springsdk.annotations.Query;
2122
import kalix.springsdk.annotations.Subscribe;
2223
import kalix.springsdk.annotations.Table;
@@ -92,6 +93,27 @@ public TransformedUser getUser(@RequestBody ByEmail byEmail) {
9293
}
9394
}
9495

96+
@Table("users_view")
97+
public static class TransformedUserViewWithJWT extends View<TransformedUser> {
98+
99+
// when methods are annotated, it's implicitly a transform = true
100+
@Subscribe.ValueEntity(UserEntity.class)
101+
public UpdateEffect<TransformedUser> onChange(User user) {
102+
return effects()
103+
.updateState(new TransformedUser(user.lastName + ", " + user.firstName, user.email));
104+
}
105+
106+
@Query("SELECT * FROM users_view WHERE email = :email")
107+
@PostMapping("/users/by-email")
108+
@JWT(
109+
validate = JWT.JwtMethodMode.BEARER_TOKEN,
110+
sign = JWT.JwtMethodMode.MESSAGE,
111+
bearerTokenIssuer = {"a", "b"})
112+
public TransformedUser getUser(@RequestBody ByEmail byEmail) {
113+
return null;
114+
}
115+
}
116+
95117
@Table("users_view")
96118
public static class TransformedUserViewUsingState extends View<TransformedUser> {
97119

0 commit comments

Comments
 (0)