Skip to content

Commit 78cd47e

Browse files
committed
feat(security,scheduler): support @RunAsUser with @scheduled
1 parent ba2ba71 commit 78cd47e

File tree

12 files changed

+1121
-0
lines changed

12 files changed

+1121
-0
lines changed

docs/src/main/asciidoc/scheduler-reference.adoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,44 @@ In this case, the method is invoked on a virtual thread.
575575
The method must return `void` and your Java runtime must provide support for virtual threads.
576576
Read xref:./virtual-threads.adoc[the virtual thread guide] for more details.
577577

578+
== Assign a user and roles to a scheduled task
579+
580+
You can use the `@RunAsUser` annotation on methods annotated with the `@Scheduled` annotation to configure the `SecurityIdentity` for the task execution.
581+
582+
.Example scheduled task with `@RunAsUser`
583+
[source,java]
584+
----
585+
import io.quarkus.scheduler.Scheduled;
586+
import io.quarkus.security.identity.RunAsUser;
587+
import jakarta.annotation.security.RolesAllowed;
588+
import jakarta.enterprise.context.ApplicationScoped;
589+
590+
class MyJob {
591+
592+
@Inject
593+
MyService myService;
594+
595+
@RunAsUser(user = "Alice", roles = "admin") <1>
596+
@Scheduled(every = "1s")
597+
void updateTask() {
598+
myService.update(); <2>
599+
}
600+
601+
}
602+
603+
@ApplicationScoped
604+
class MyService {
605+
606+
@RolesAllowed("admin")
607+
void update() { }
608+
609+
}
610+
----
611+
<1> Assign the user `Alice` to the scheduled `updateTask`. Only scheduled tasks should rely on the `@RunAsUser` annotation.
612+
<2> The call to the `MyService#update` method succeeds, because the configured user has role `admin`.
613+
614+
IMPORTANT: The `@RunAsUser` annotation creates an identity for scheduled tasks. It does not temporarily replace the current identity.
615+
578616
== Configuration Reference
579617

580618
include::{generated-dir}/config/quarkus-scheduler.adoc[leveloffset=+1, opts=optional]
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package io.quarkus.quartz.test.security;
2+
3+
import static io.quarkus.scheduler.Scheduled.QUARTZ;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.assertj.core.api.Assertions.fail;
6+
import static org.junit.jupiter.api.Assertions.assertNull;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.concurrent.CompletionStage;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
import java.util.concurrent.CountDownLatch;
14+
import java.util.concurrent.TimeUnit;
15+
16+
import jakarta.annotation.security.RolesAllowed;
17+
import jakarta.enterprise.context.ApplicationScoped;
18+
import jakarta.inject.Inject;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.RegisterExtension;
22+
23+
import io.quarkus.maven.dependency.Dependency;
24+
import io.quarkus.scheduler.Scheduled;
25+
import io.quarkus.security.Authenticated;
26+
import io.quarkus.security.ForbiddenException;
27+
import io.quarkus.security.UnauthorizedException;
28+
import io.quarkus.security.identity.RunAsUser;
29+
import io.quarkus.test.QuarkusUnitTest;
30+
import io.smallrye.mutiny.Uni;
31+
32+
class QuartzSchedulerRunAsUserTest {
33+
34+
private static final String UNAUTHENTICATED_SCHEDULER = "unauthenticated";
35+
private static final String AUTHENTICATED_SCHEDULER = "authenticated";
36+
private static final String FORBIDDEN_SCHEDULER = "forbidden";
37+
private static final String AUTHORIZED_SCHEDULER = "authorized";
38+
39+
@RegisterExtension
40+
static final QuarkusUnitTest test = new QuarkusUnitTest()
41+
.withApplicationRoot((jar) -> jar.addClasses(Scheduler.class, SecuredBean.class, StaticScheduler.class))
42+
.setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-security")));
43+
44+
@Inject
45+
Scheduler scheduler;
46+
47+
@Test
48+
void testRunAsUserAnnotationOnBeanMethods() throws InterruptedException {
49+
for (var e : scheduler.getLatchMap().entrySet()) {
50+
var latchKey = e.getKey();
51+
var latch = e.getValue();
52+
var result = latch.await(5, TimeUnit.SECONDS);
53+
assertTrue(result, () -> "Latch " + latchKey + " did not count down in time");
54+
var failure = scheduler.getLatchKeyToFailure().get(latchKey);
55+
assertNull(failure, () -> "Test for latch '" + latchKey + "' failed over: " + failure);
56+
}
57+
}
58+
59+
@Test
60+
void testRunAsUserAnnotationOnStaticMethod() throws InterruptedException {
61+
var result = StaticScheduler.LATCH.await(5, TimeUnit.SECONDS);
62+
assertTrue(result, "Latch on static scheduler did not count down in time");
63+
if (StaticScheduler.authenticatedTestFailure != null) {
64+
fail("Static scheduler should succeed when calling method that requires authentication",
65+
StaticScheduler.authenticatedTestFailure);
66+
}
67+
if (StaticScheduler.rolesAllowedTestFailure != null) {
68+
fail("Static scheduler should succeed when calling to method that requires 'user' role",
69+
StaticScheduler.rolesAllowedTestFailure);
70+
}
71+
if (StaticScheduler.forbiddenTestFailure == null) {
72+
fail("Static scheduler should fail when calling to method that requires 'admin' role");
73+
}
74+
assertThat(StaticScheduler.forbiddenTestFailure).isInstanceOf(ForbiddenException.class);
75+
}
76+
77+
static class StaticScheduler {
78+
79+
private static final CountDownLatch LATCH = new CountDownLatch(1);
80+
private static volatile boolean run = false;
81+
private static Throwable forbiddenTestFailure = null;
82+
private static Throwable authenticatedTestFailure = null;
83+
private static Throwable rolesAllowedTestFailure = null;
84+
85+
@RunAsUser(user = "Elliott", roles = "user")
86+
@Scheduled(every = "1s", executeWith = QUARTZ)
87+
static void everySecond() {
88+
if (!run) {
89+
run = true;
90+
try {
91+
authenticated();
92+
} catch (Throwable throwable) {
93+
authenticatedTestFailure = throwable;
94+
}
95+
try {
96+
rolesAllowedUser();
97+
} catch (Throwable throwable) {
98+
rolesAllowedTestFailure = throwable;
99+
}
100+
try {
101+
rolesAllowedAdmin();
102+
} catch (Throwable throwable) {
103+
forbiddenTestFailure = throwable;
104+
}
105+
LATCH.countDown();
106+
}
107+
}
108+
109+
@Authenticated
110+
static void authenticated() {
111+
112+
}
113+
114+
@RolesAllowed("user")
115+
static void rolesAllowedUser() {
116+
117+
}
118+
119+
@RolesAllowed("admin")
120+
static void rolesAllowedAdmin() {
121+
122+
}
123+
124+
}
125+
126+
@ApplicationScoped
127+
static class Scheduler {
128+
129+
private final Map<String, CountDownLatch> latchMap;
130+
private final Map<String, Throwable> latchKeyToFailure;
131+
private final SecuredBean securedBean;
132+
133+
Scheduler(SecuredBean securedBean) {
134+
this.latchKeyToFailure = new ConcurrentHashMap<>();
135+
this.latchMap = Map.of(
136+
UNAUTHENTICATED_SCHEDULER, new CountDownLatch(2),
137+
FORBIDDEN_SCHEDULER, new CountDownLatch(2),
138+
AUTHORIZED_SCHEDULER, new CountDownLatch(2),
139+
AUTHENTICATED_SCHEDULER, new CountDownLatch(2));
140+
this.securedBean = securedBean;
141+
}
142+
143+
@Scheduled(every = "1s", executeWith = QUARTZ)
144+
void noRunAsUserAnnotation() {
145+
runTest(UNAUTHENTICATED_SCHEDULER, () -> {
146+
try {
147+
securedBean.authenticated();
148+
} catch (UnauthorizedException ignored) {
149+
return;
150+
}
151+
throw new AssertionError("Authorization should fail for scheduled method 'noRunAsUserAnnotation'");
152+
});
153+
}
154+
155+
@RunAsUser(user = "Quentin")
156+
@Scheduled(every = "1s", executeWith = QUARTZ)
157+
void runAsUserAnnotationWithVoidReturnType() {
158+
runTest(AUTHENTICATED_SCHEDULER, () -> {
159+
try {
160+
securedBean.authenticated();
161+
} catch (UnauthorizedException exception) {
162+
throw new AssertionError(
163+
"Authorization should not fail for scheduled method 'runAsUserAnnotationWithVoidReturnType'",
164+
exception);
165+
}
166+
});
167+
}
168+
169+
@RunAsUser(user = "Julia", roles = "user")
170+
@Scheduled(every = "1s", executeWith = QUARTZ)
171+
Uni<Void> runAsUserAnnotationWithUniReturnType() {
172+
return Uni.createFrom().item(() -> {
173+
runTest(FORBIDDEN_SCHEDULER, () -> {
174+
try {
175+
securedBean.rolesAllowedAdmin();
176+
} catch (ForbiddenException exception) {
177+
return;
178+
}
179+
throw new AssertionError(
180+
"Authorization should fail for scheduled method 'runAsUserAnnotationWithUniReturnType'");
181+
});
182+
return null;
183+
});
184+
}
185+
186+
@RunAsUser(user = "Alice", roles = "admin")
187+
@Scheduled(every = "1s", executeWith = QUARTZ)
188+
CompletionStage<Void> runAsUserAnnotationWithCompletionStageReturnType() {
189+
return Uni.createFrom().<Void> item(() -> {
190+
runTest(AUTHORIZED_SCHEDULER, () -> {
191+
try {
192+
securedBean.rolesAllowedAdmin();
193+
} catch (ForbiddenException exception) {
194+
throw new AssertionError(
195+
"Authorization should not fail for scheduled method 'runAsUserAnnotationWithCompletionStageReturnType'",
196+
exception);
197+
}
198+
});
199+
return null;
200+
}).subscribeAsCompletionStage();
201+
}
202+
203+
private void runTest(String latchKey, Runnable test) {
204+
try {
205+
test.run();
206+
} catch (Throwable failure) {
207+
latchKeyToFailure.put(latchKey, failure);
208+
}
209+
latchMap.get(latchKey).countDown();
210+
}
211+
212+
Map<String, CountDownLatch> getLatchMap() {
213+
return latchMap;
214+
}
215+
216+
Map<String, Throwable> getLatchKeyToFailure() {
217+
return latchKeyToFailure;
218+
}
219+
}
220+
221+
@ApplicationScoped
222+
static class SecuredBean {
223+
224+
@Authenticated
225+
void authenticated() {
226+
227+
}
228+
229+
@RolesAllowed("admin")
230+
void rolesAllowedAdmin() {
231+
232+
}
233+
234+
}
235+
236+
}

extensions/scheduler/deployment/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
<groupId>io.quarkus</groupId>
3939
<artifactId>quarkus-assistant-deployment-spi</artifactId>
4040
</dependency>
41+
<dependency>
42+
<groupId>io.quarkus</groupId>
43+
<artifactId>quarkus-security-spi</artifactId>
44+
</dependency>
4145
<dependency>
4246
<groupId>io.quarkus</groupId>
4347
<artifactId>quarkus-vertx-http-deployment</artifactId>

extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
import io.quarkus.scheduler.runtime.SchedulerConfig;
9696
import io.quarkus.scheduler.runtime.SchedulerRecorder;
9797
import io.quarkus.scheduler.runtime.SimpleScheduler;
98+
import io.quarkus.security.spi.RunAsUserPredicateBuildItem;
9899
import io.smallrye.common.annotation.Identifier;
99100

100101
public class SchedulerProcessor {
@@ -756,4 +757,8 @@ void produceCoroutineScope(BuildProducer<AdditionalBeanBuildItem> buildItemBuild
756757
.setUnremovable().build());
757758
}
758759

760+
@BuildStep
761+
RunAsUserPredicateBuildItem allowRunAsUserAnnotationForScheduledMethods() {
762+
return RunAsUserPredicateBuildItem.ofAnnotation(Scheduled.class);
763+
}
759764
}

0 commit comments

Comments
 (0)