Skip to content

Commit b11f82c

Browse files
committed
Added RequestContext.deferSecured()
1 parent 23c0010 commit b11f82c

File tree

5 files changed

+141
-4
lines changed

5 files changed

+141
-4
lines changed

services-api/src/main/java/io/scalecube/services/RequestContext.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import static io.scalecube.services.auth.Principal.NULL_PRINCIPAL;
66

77
import io.scalecube.services.auth.Principal;
8+
import io.scalecube.services.exceptions.ForbiddenException;
9+
import io.scalecube.services.methods.MethodInfo;
810
import java.math.BigDecimal;
911
import java.math.BigInteger;
1012
import java.util.Map;
@@ -116,6 +118,14 @@ public boolean hasPrincipal() {
116118
return principal != null && principal != NULL_PRINCIPAL;
117119
}
118120

121+
public MethodInfo methodInfo() {
122+
return getOrDefault("methodInfo", null);
123+
}
124+
125+
public RequestContext methodInfo(MethodInfo methodInfo) {
126+
return put("methodInfo", methodInfo);
127+
}
128+
119129
public Map<String, String> pathVars() {
120130
return get("pathVars");
121131
}
@@ -163,6 +173,28 @@ public static Mono<RequestContext> deferContextual() {
163173
return Mono.deferContextual(context -> Mono.just(context.get(RequestContext.class)));
164174
}
165175

176+
public static Mono<RequestContext> deferSecured() {
177+
return Mono.deferContextual(context -> Mono.just(context.get(RequestContext.class)))
178+
.doOnNext(
179+
context -> {
180+
if (!context.hasPrincipal()) {
181+
throw new ForbiddenException("Insufficient permissions");
182+
}
183+
184+
final var principal = context.principal();
185+
final var methodInfo = context.methodInfo();
186+
187+
if (!methodInfo.allowedRoles().contains(principal.role())) {
188+
throw new ForbiddenException("Insufficient permissions");
189+
}
190+
for (var allowedPermission : methodInfo.allowedPermissions()) {
191+
if (!principal.hasPermission(allowedPermission)) {
192+
throw new ForbiddenException("Insufficient permissions");
193+
}
194+
}
195+
});
196+
}
197+
166198
@Override
167199
public String toString() {
168200
return new StringJoiner(", ", RequestContext.class.getSimpleName() + "[", "]")

services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,11 @@ private Context enhanceRequestContext(
189189
pathVars = dynamicQualifier.matchQualifier(context.requestQualifier());
190190
}
191191

192-
return new RequestContext(context).request(request).principal(principal).pathVars(pathVars);
192+
return new RequestContext(context)
193+
.request(request)
194+
.principal(principal)
195+
.pathVars(pathVars)
196+
.methodInfo(methodInfo);
193197
}
194198

195199
private Object toRequest(ServiceMessage message) {

services-api/src/test/java/io/scalecube/services/methods/ServiceMethodInvokerTest.java

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package io.scalecube.services.methods;
22

33
import static io.scalecube.services.auth.Principal.NULL_PRINCIPAL;
4+
import static io.scalecube.services.methods.StubService.NAMESPACE;
45
import static org.junit.jupiter.api.Assertions.assertEquals;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
67
import static org.mockito.Mockito.mock;
78
import static org.mockito.Mockito.when;
89

10+
import io.scalecube.services.CommunicationMode;
911
import io.scalecube.services.Reflect;
1012
import io.scalecube.services.RequestContext;
1113
import io.scalecube.services.api.ErrorData;
@@ -17,6 +19,7 @@
1719
import io.scalecube.services.transport.api.ServiceMessageDataDecoder;
1820
import java.lang.reflect.Method;
1921
import java.util.List;
22+
import java.util.Set;
2023
import java.util.function.Consumer;
2124
import java.util.stream.Stream;
2225
import org.junit.jupiter.api.DisplayName;
@@ -27,6 +30,7 @@
2730
import org.mockito.ArgumentMatchers;
2831
import reactor.core.publisher.Flux;
2932
import reactor.core.publisher.Mono;
33+
import reactor.core.scheduler.Schedulers;
3034
import reactor.test.StepVerifier;
3135
import reactor.util.context.Context;
3236

@@ -286,6 +290,88 @@ static Stream<Principal> testAuthFailedWithRoleOrPermissionsMethodSource() {
286290
}
287291
}
288292

293+
@Nested
294+
class ServiceRoleTests {
295+
296+
@Test
297+
@DisplayName("Invocation of service method with @AllowedRole annotation")
298+
void invokeWithAllowedRoleAnnotation() throws Exception {
299+
final var methodName = "invokeWithAllowedRoleAnnotation";
300+
final var method = StubService.class.getMethod(methodName);
301+
final var methodInfo =
302+
new MethodInfo(
303+
NAMESPACE,
304+
methodName,
305+
Void.TYPE,
306+
false,
307+
CommunicationMode.REQUEST_RESPONSE,
308+
0,
309+
Void.TYPE,
310+
false,
311+
true,
312+
Schedulers.immediate(),
313+
null,
314+
List.of(new ServiceRoleDefinition("admin", Set.of("read", "write"))));
315+
316+
final var message = serviceMessage(methodName);
317+
final var principal = new PrincipalImpl("admin", List.of("read", "write"));
318+
319+
final var authenticator = mock(Authenticator.class);
320+
when(authenticator.authenticate(ArgumentMatchers.any())).thenReturn(Mono.just(principal));
321+
322+
serviceMethodInvoker = serviceMethodInvoker(method, methodInfo, authenticator);
323+
324+
StepVerifier.create(
325+
serviceMethodInvoker
326+
.invokeOne(message)
327+
.contextWrite(requestContext(message, principal)))
328+
.verifyComplete();
329+
}
330+
331+
@ParameterizedTest
332+
@MethodSource("invokeWithAllowedRoleAnnotationFailedMethodSource")
333+
@DisplayName("Failed to invoke of service method with @AllowedRole annotation")
334+
void invokeWithAllowedRoleAnnotationFailed(Principal principal) throws Exception {
335+
final var methodName = "invokeWithAllowedRoleAnnotation";
336+
final var method = StubService.class.getMethod(methodName);
337+
final var methodInfo =
338+
new MethodInfo(
339+
NAMESPACE,
340+
methodName,
341+
Void.TYPE,
342+
false,
343+
CommunicationMode.REQUEST_RESPONSE,
344+
0,
345+
Void.TYPE,
346+
false,
347+
true,
348+
Schedulers.immediate(),
349+
null,
350+
List.of(new ServiceRoleDefinition("admin", Set.of("read", "write"))));
351+
352+
final var message = serviceMessage(methodName);
353+
354+
final var authenticator = mock(Authenticator.class);
355+
when(authenticator.authenticate(ArgumentMatchers.any())).thenReturn(Mono.just(principal));
356+
357+
serviceMethodInvoker = serviceMethodInvoker(method, methodInfo, authenticator);
358+
359+
StepVerifier.create(
360+
serviceMethodInvoker
361+
.invokeOne(message)
362+
.contextWrite(requestContext(message, principal)))
363+
.assertNext(assertError(403, "Insufficient permissions"))
364+
.verifyComplete();
365+
}
366+
367+
static Stream<Principal> invokeWithAllowedRoleAnnotationFailedMethodSource() {
368+
return Stream.of(
369+
NULL_PRINCIPAL,
370+
new PrincipalImpl("invalid-role", null),
371+
new PrincipalImpl("admin", List.of("invalid-permission")));
372+
}
373+
}
374+
289375
private static Context requestContext(ServiceMessage message, Principal principal) {
290376
return new RequestContext().headers(message.headers()).principal(principal);
291377
}
@@ -303,9 +389,7 @@ private ServiceMethodInvoker serviceMethodInvoker(
303389
}
304390

305391
private static ServiceMessage serviceMessage(String action) {
306-
return ServiceMessage.builder()
307-
.qualifier(Qualifier.asString(StubService.NAMESPACE, action))
308-
.build();
392+
return ServiceMessage.builder().qualifier(Qualifier.asString(NAMESPACE, action)).build();
309393
}
310394

311395
private static Consumer<ServiceMessage> assertError(int errorCode, String errorMessage) {

services-api/src/test/java/io/scalecube/services/methods/StubService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,10 @@ public interface StubService {
4545
@Secured
4646
@ServiceMethod
4747
Mono<Void> invokeWithRoleOrPermissions();
48+
49+
// Services secured by annotations in method body
50+
51+
@Secured
52+
@ServiceMethod
53+
Mono<Void> invokeWithAllowedRoleAnnotation();
4854
}

services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.junit.jupiter.api.Assertions.assertTrue;
55

66
import io.scalecube.services.RequestContext;
7+
import io.scalecube.services.auth.AllowedRole;
78
import io.scalecube.services.exceptions.ForbiddenException;
89
import reactor.core.publisher.Flux;
910
import reactor.core.publisher.Mono;
@@ -96,4 +97,14 @@ public Mono<Void> invokeWithRoleOrPermissions() {
9697
})
9798
.then();
9899
}
100+
101+
// Services secured by annotations in method body
102+
103+
@AllowedRole(
104+
name = "admin",
105+
permissions = {"read", "write"})
106+
@Override
107+
public Mono<Void> invokeWithAllowedRoleAnnotation() {
108+
return RequestContext.deferSecured().then();
109+
}
99110
}

0 commit comments

Comments
 (0)