Skip to content

Commit f0f38a4

Browse files
committed
Update PKC JWKS reload settings names. Add tests for JWKS loader task stop.
1 parent 88ea7ce commit f0f38a4

File tree

4 files changed

+41
-24
lines changed

4 files changed

+41
-24
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,25 +262,25 @@ private static Set<Setting.AffixSetting<SecureString>> getSecureSettings() {
262262

263263
public static final Setting.AffixSetting<Boolean> PKC_JWKSET_RELOAD_ENABLED = Setting.affixKeySetting(
264264
RealmSettings.realmSettingPrefix(TYPE),
265-
"pkc_jwkset_reload_enabled",
265+
"pkc_jwkset_reload.enabled",
266266
key -> Setting.boolSetting(key, false, Setting.Property.NodeScope)
267267
);
268268

269269
public static final Setting.AffixSetting<TimeValue> PKC_JWKSET_RELOAD_FILE_INTERVAL = Setting.affixKeySetting(
270270
RealmSettings.realmSettingPrefix(TYPE),
271-
"pkc_jwkset_reload_file_interval",
271+
"pkc_jwkset_reload.file_interval",
272272
key -> Setting.timeSetting(key, TimeValue.timeValueMinutes(5), TimeValue.timeValueMinutes(5), Setting.Property.NodeScope)
273273
);
274274

275275
public static final Setting.AffixSetting<TimeValue> PKC_JWKSET_RELOAD_URL_INTERVAL_MIN = Setting.affixKeySetting(
276276
RealmSettings.realmSettingPrefix(TYPE),
277-
"pkc_jwkset_reload_url_interval_min",
277+
"pkc_jwkset_reload.url_interval_min",
278278
key -> Setting.timeSetting(key, TimeValue.timeValueHours(1), TimeValue.timeValueMinutes(5), Setting.Property.NodeScope)
279279
);
280280

281281
public static final Setting.AffixSetting<TimeValue> PKC_JWKSET_RELOAD_URL_INTERVAL_MAX = Setting.affixKeySetting(
282282
RealmSettings.realmSettingPrefix(TYPE),
283-
"pkc_jwkset_reload_url_interval_max",
283+
"pkc_jwkset_reload.url_interval_max",
284284
key -> Setting.timeSetting(key, TimeValue.timeValueDays(5), TimeValue.timeValueMinutes(5), Setting.Property.NodeScope)
285285
);
286286

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwkSetLoader.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ void reload(FileChangeWatcher fileWatcher) {
239239
return;
240240
}
241241
try {
242-
if (fileWatcher.changed() == false) {
242+
if (fileWatcher.changedSinceLastCall() == false) {
243243
logger.debug("No changes detected in PKC JWK set file [{}], aborting", jwkSetPath);
244244
return;
245245
}
@@ -267,6 +267,7 @@ public void load(ActionListener<byte[]> listener) {
267267

268268
@Override
269269
public void stop() {
270+
closed = true;
270271
if (task != null) {
271272
task.cancel();
272273
}
@@ -394,7 +395,7 @@ static class FileChangeWatcher implements FileChangesListener {
394395
this.fileWatcher.addListener(this);
395396
}
396397

397-
boolean changed() throws IOException {
398+
boolean changedSinceLastCall() throws IOException {
398399
fileWatcher.checkAndNotify(); // may call onFileInit, onFileChanged
399400
boolean c = changed;
400401
changed = false;

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.elasticsearch.common.settings.SecureString;
4747
import org.elasticsearch.common.settings.SettingsException;
4848
import org.elasticsearch.common.util.concurrent.ThreadContext;
49+
import org.elasticsearch.core.Nullable;
4950
import org.elasticsearch.env.Environment;
5051
import org.elasticsearch.xcontent.XContentBuilder;
5152
import org.elasticsearch.xcontent.XContentFactory;
@@ -498,10 +499,10 @@ private static boolean containsAtLeastTwoDots(SecureString secureString) {
498499
return false;
499500
}
500501

501-
record JwksResponse(byte[] content, Instant expires, Integer maxAgeSeconds) {
502+
record JwksResponse(byte[] content, @Nullable Instant expires, @Nullable Integer maxAgeSeconds) {
502503
private static final Pattern MAX_AGE_PATTERN = Pattern.compile("\\bmax-age\\s*=\\s*(\\d+)\\b", Pattern.CASE_INSENSITIVE);
503504

504-
JwksResponse(byte[] content, String expires, String cacheControl) {
505+
JwksResponse(byte[] content, @Nullable String expires, @Nullable String cacheControl) {
505506
this(content, parseExpires(expires), parseMaxAge(cacheControl));
506507
}
507508

@@ -510,7 +511,7 @@ record JwksResponse(byte[] content, Instant expires, Integer maxAgeSeconds) {
510511
* The Expires header follows RFC 7231 format (e.g., "Thu, 01 Jan 2024 00:00:00 GMT").
511512
* @return the parsed Instant, or null if the header is null or cannot be parsed
512513
*/
513-
static Instant parseExpires(String expires) {
514+
static Instant parseExpires(@Nullable String expires) {
514515
if (expires == null) {
515516
return null;
516517
}
@@ -525,10 +526,10 @@ static Instant parseExpires(String expires) {
525526
}
526527

527528
/**
528-
* Parse the Cache-Control header to extract the max-age value.
529+
* Parse the Cache-Control header to extract the max-age value defined in RFC 7234.
529530
* @return the parsed max-age value as Integer, or null if the header is null or cannot be parsed
530531
*/
531-
static Integer parseMaxAge(String cacheControl) {
532+
static Integer parseMaxAge(@Nullable String cacheControl) {
532533
if (cacheControl == null) {
533534
return null;
534535
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwkSetLoaderTests.java

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,13 @@ public void testFileChangeWatcher() throws IOException {
157157
var now = Instant.now();
158158
int ticks = 0;
159159
var watcher = new JwkSetLoader.FileChangeWatcher(path);
160-
assertThat(watcher.changed(), is(true));
160+
assertThat(watcher.changedSinceLastCall(), is(true));
161161
Files.setLastModifiedTime(path, FileTime.from(now.plusSeconds(++ticks)));
162-
assertThat(watcher.changed(), is(true));
163-
assertThat(watcher.changed(), is(false));
162+
assertThat(watcher.changedSinceLastCall(), is(true));
163+
assertThat(watcher.changedSinceLastCall(), is(false));
164164
Files.setLastModifiedTime(path, FileTime.from(now.plusSeconds(++ticks)));
165-
assertThat(watcher.changed(), is(true));
166-
assertThat(watcher.changed(), is(false));
165+
assertThat(watcher.changedSinceLastCall(), is(true));
166+
assertThat(watcher.changedSinceLastCall(), is(false));
167167
}
168168

169169
public void testFilePkcJwkSetLoader() throws IOException {
@@ -181,7 +181,9 @@ public void testFilePkcJwkSetLoader() throws IOException {
181181
ThreadPool threadPool = mock(ThreadPool.class);
182182
CountingCallback callback = new CountingCallback();
183183

184-
new JwkSetLoader.FilePkcJwkSetLoader(realmConfig, threadPool, path.toString(), callback).start(); // schedules task
184+
var loader = new JwkSetLoader.FilePkcJwkSetLoader(realmConfig, threadPool, path.toString(), callback);
185+
loader.start(); // schedules task
186+
185187
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
186188
verify(threadPool, times(1)).scheduleWithFixedDelay(taskCaptor.capture(), any(TimeValue.class), isNull());
187189

@@ -198,6 +200,15 @@ public void testFilePkcJwkSetLoader() throws IOException {
198200
taskCaptor.getValue().run(); // run third time, change detected
199201
assertThat(callback.content, is(equalTo(goodbye)));
200202
assertThat(callback.count, is(2));
203+
204+
loader.stop();
205+
206+
Files.writeString(path, "too late");
207+
Files.setLastModifiedTime(path, FileTime.from(Instant.now().plusSeconds(1))); // ensure mod time changes
208+
209+
taskCaptor.getValue().run(); // run fourth time, callback not invoked due to closure
210+
assertThat(callback.content, is(equalTo(goodbye)));
211+
assertThat(callback.count, is(2));
201212
}
202213

203214
public void testUrlPkcJwkSetLoader() throws IOException {
@@ -209,12 +220,18 @@ public void testUrlPkcJwkSetLoader() throws IOException {
209220

210221
CountingCallback callback = new CountingCallback();
211222

212-
new JwkSetLoader.UrlPkcJwkSetLoader(realmConfig, threadPool, uri, httpClient, callback).start(); // schedules task
223+
var loader = new JwkSetLoader.UrlPkcJwkSetLoader(realmConfig, threadPool, uri, httpClient, callback);
224+
loader.start(); // schedules task
213225

214226
int iterations = randomIntBetween(5, 10);
215227
for (int i = 0; i < iterations; i++) {
216228
verifySchedulingIteration(callback, threadPool, httpClient, i + 1);
217229
}
230+
231+
loader.stop();
232+
verify(httpClient, times(1)).close();
233+
verifySchedulingIteration(callback, threadPool, httpClient, iterations + 1); // last schedule call, no subsequent reschedule
234+
verify(threadPool, never()).schedule(any(Runnable.class), any(TimeValue.class), isNull());
218235
}
219236

220237
public void testUrlPkcJwkSetLoaderListenerException() throws IOException {
@@ -230,7 +247,7 @@ public void testUrlPkcJwkSetLoaderListenerException() throws IOException {
230247

231248
int iterations = randomIntBetween(5, 10);
232249
for (int i = 0; i < iterations; i++) {
233-
verifySchedulingIterationWithListenerException(threadPool, httpClient, i + 1);
250+
verifySchedulingIterationWithListenerException(threadPool, httpClient);
234251
}
235252
}
236253

@@ -263,16 +280,15 @@ private void verifySchedulingIteration(
263280
byte[] bytes = "x".repeat(iteration).getBytes(StandardCharsets.UTF_8);
264281
HttpResponse response = makeHttpResponse(bytes, randomBoolean());
265282

266-
reset(threadPool);
267-
reset(httpClient);
283+
reset(threadPool, httpClient);
268284

269285
// invoke response handler
270286
responseFn.getValue().completed(response);
271287
assertThat(callback.content, is(equalTo(bytes)));
272288
assertThat(callback.count, is(iteration));
273289
}
274290

275-
private void verifySchedulingIterationWithListenerException(ThreadPool threadPool, CloseableHttpAsyncClient httpClient, int iteration)
291+
private void verifySchedulingIterationWithListenerException(ThreadPool threadPool, CloseableHttpAsyncClient httpClient)
276292
throws IOException {
277293
// capture scheduled task and delay
278294
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
@@ -289,8 +305,7 @@ private void verifySchedulingIterationWithListenerException(ThreadPool threadPoo
289305
verify(httpClient, times(1)).execute(any(HttpGet.class), responseFn.capture());
290306
HttpResponse response = makeHttpResponse(new byte[0], randomBoolean());
291307

292-
reset(threadPool);
293-
reset(httpClient);
308+
reset(threadPool, httpClient);
294309

295310
// invoke response handler
296311
responseFn.getValue().completed(response);

0 commit comments

Comments
 (0)