Skip to content

Commit 9361d76

Browse files
committed
feat(hunspell): Add ref_path support and cache invalidation API
- Add ref_path parameter to hunspell token filter for package-based dictionaries - Load dictionaries from config/packages/{packageId}/hunspell/{locale}/ - Add cache invalidation REST API endpoints: - GET /_hunspell/cache (view cached dictionaries) - POST /_hunspell/cache/_invalidate (invalidate by package_id, locale, or cache_key) - POST /_hunspell/cache/_invalidate_all (clear all cached dictionaries) - Add TransportAction with cluster:admin/hunspell/cache/clear permission - Add path traversal security validation for packageId and locale - Add updateable flag support for hot-reload via _reload_search_analyzers - Add comprehensive test coverage for HunspellTokenFilterFactory and REST endpoint - Remove unused index-level ref_path setting Addresses PR feedback: - Removed mise.toml from commit (local dev config) - Added TransportAction authorization pattern - Added 21 unit tests for REST endpoint - Fixed security validation for path traversal attacks - Documented race condition in invalidateAllDictionaries Signed-off-by: shayush622 <ayush5267@gmail.com>
1 parent c114694 commit 9361d76

File tree

17 files changed

+63273
-169
lines changed

17 files changed

+63273
-169
lines changed

server/src/main/java/org/opensearch/action/ActionModule.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@
138138
import org.opensearch.action.admin.indices.analyze.TransportAnalyzeAction;
139139
import org.opensearch.action.admin.indices.cache.clear.ClearIndicesCacheAction;
140140
import org.opensearch.action.admin.indices.cache.clear.TransportClearIndicesCacheAction;
141+
import org.opensearch.action.admin.indices.cache.hunspell.HunspellCacheInvalidateAction;
142+
import org.opensearch.action.admin.indices.cache.hunspell.TransportHunspellCacheInvalidateAction;
141143
import org.opensearch.action.admin.indices.close.CloseIndexAction;
142144
import org.opensearch.action.admin.indices.close.TransportCloseIndexAction;
143145
import org.opensearch.action.admin.indices.create.AutoCreateAction;
@@ -442,6 +444,7 @@
442444
import org.opensearch.rest.action.admin.indices.RestSimulateTemplateAction;
443445
import org.opensearch.rest.action.admin.indices.RestSyncedFlushAction;
444446
import org.opensearch.rest.action.admin.indices.RestUpdateSettingsAction;
447+
import org.opensearch.rest.action.admin.indices.RestHunspellCacheInvalidateAction;
445448
import org.opensearch.rest.action.admin.indices.RestUpgradeAction;
446449
import org.opensearch.rest.action.admin.indices.RestUpgradeStatusAction;
447450
import org.opensearch.rest.action.admin.indices.RestValidateQueryAction;
@@ -487,6 +490,7 @@
487490
import org.opensearch.rest.action.list.RestIndicesListAction;
488491
import org.opensearch.rest.action.list.RestListAction;
489492
import org.opensearch.rest.action.list.RestShardsListAction;
493+
import org.opensearch.indices.analysis.HunspellService;
490494
import org.opensearch.rest.action.search.RestClearScrollAction;
491495
import org.opensearch.rest.action.search.RestCountAction;
492496
import org.opensearch.rest.action.search.RestCreatePitAction;
@@ -557,6 +561,7 @@ public class ActionModule extends AbstractModule {
557561
private final ThreadPool threadPool;
558562
private final ExtensionsManager extensionsManager;
559563
private final ResponseLimitSettings responseLimitSettings;
564+
private final HunspellService hunspellService;
560565

561566
public ActionModule(
562567
Settings settings,
@@ -571,7 +576,8 @@ public ActionModule(
571576
UsageService usageService,
572577
SystemIndices systemIndices,
573578
IdentityService identityService,
574-
ExtensionsManager extensionsManager
579+
ExtensionsManager extensionsManager,
580+
HunspellService hunspellService
575581
) {
576582
this.settings = settings;
577583
this.indexNameExpressionResolver = indexNameExpressionResolver;
@@ -581,6 +587,7 @@ public ActionModule(
581587
this.actionPlugins = actionPlugins;
582588
this.threadPool = threadPool;
583589
this.extensionsManager = extensionsManager;
590+
this.hunspellService = hunspellService;
584591
actions = setupActions(actionPlugins);
585592
actionFilters = setupActionFilters(actionPlugins);
586593
dynamicActionRegistry = new DynamicActionRegistry();
@@ -725,6 +732,7 @@ public <Request extends ActionRequest, Response extends ActionResponse> void reg
725732
actions.register(UpgradeStatusAction.INSTANCE, TransportUpgradeStatusAction.class);
726733
actions.register(UpgradeSettingsAction.INSTANCE, TransportUpgradeSettingsAction.class);
727734
actions.register(ClearIndicesCacheAction.INSTANCE, TransportClearIndicesCacheAction.class);
735+
actions.register(HunspellCacheInvalidateAction.INSTANCE, TransportHunspellCacheInvalidateAction.class);
728736
actions.register(GetAliasesAction.INSTANCE, TransportGetAliasesAction.class);
729737
actions.register(GetSettingsAction.INSTANCE, TransportGetSettingsAction.class);
730738

@@ -1075,6 +1083,11 @@ public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster) {
10751083
registerHandler.accept(new RestPauseIngestionAction());
10761084
registerHandler.accept(new RestResumeIngestionAction());
10771085
registerHandler.accept(new RestGetIngestionStateAction());
1086+
1087+
// Hunspell cache management API
1088+
if (hunspellService != null) {
1089+
registerHandler.accept(new RestHunspellCacheInvalidateAction(hunspellService));
1090+
}
10781091
}
10791092

10801093
@Override
@@ -1110,6 +1123,11 @@ protected void configure() {
11101123
bind(DynamicActionRegistry.class).toInstance(dynamicActionRegistry);
11111124

11121125
bind(ResponseLimitSettings.class).toInstance(responseLimitSettings);
1126+
1127+
// Bind HunspellService for TransportHunspellCacheInvalidateAction injection
1128+
if (hunspellService != null) {
1129+
bind(HunspellService.class).toInstance(hunspellService);
1130+
}
11131131
}
11141132

11151133
public ActionFilters getActionFilters() {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.action.admin.indices.cache.hunspell;
10+
11+
import org.opensearch.action.ActionType;
12+
13+
/**
14+
* Action type for invalidating Hunspell dictionary cache.
15+
*
16+
* This action requires cluster admin permissions when the security plugin is enabled.
17+
* The action name "cluster:admin/hunspell/cache/clear" maps to IAM policies for authorization.
18+
*
19+
* @opensearch.internal
20+
*/
21+
public class HunspellCacheInvalidateAction extends ActionType<HunspellCacheInvalidateResponse> {
22+
23+
public static final HunspellCacheInvalidateAction INSTANCE = new HunspellCacheInvalidateAction();
24+
public static final String NAME = "cluster:admin/hunspell/cache/clear";
25+
26+
private HunspellCacheInvalidateAction() {
27+
super(NAME, HunspellCacheInvalidateResponse::new);
28+
}
29+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.action.admin.indices.cache.hunspell;
10+
11+
import org.opensearch.action.ActionRequest;
12+
import org.opensearch.action.ActionRequestValidationException;
13+
import org.opensearch.core.common.io.stream.StreamInput;
14+
import org.opensearch.core.common.io.stream.StreamOutput;
15+
16+
import java.io.IOException;
17+
18+
/**
19+
* Request for Hunspell cache invalidation.
20+
*
21+
* <p>Supports three modes:
22+
* <ul>
23+
* <li>Invalidate by package_id (clears all locales for a package)</li>
24+
* <li>Invalidate by package_id + locale (clears specific entry)</li>
25+
* <li>Invalidate by cache_key (direct key)</li>
26+
* <li>Invalidate all (invalidateAll = true)</li>
27+
* </ul>
28+
*
29+
* @opensearch.internal
30+
*/
31+
public class HunspellCacheInvalidateRequest extends ActionRequest {
32+
33+
private String packageId;
34+
private String locale;
35+
private String cacheKey;
36+
private boolean invalidateAll;
37+
38+
public HunspellCacheInvalidateRequest() {
39+
}
40+
41+
public HunspellCacheInvalidateRequest(StreamInput in) throws IOException {
42+
super(in);
43+
this.packageId = in.readOptionalString();
44+
this.locale = in.readOptionalString();
45+
this.cacheKey = in.readOptionalString();
46+
this.invalidateAll = in.readBoolean();
47+
}
48+
49+
@Override
50+
public void writeTo(StreamOutput out) throws IOException {
51+
super.writeTo(out);
52+
out.writeOptionalString(packageId);
53+
out.writeOptionalString(locale);
54+
out.writeOptionalString(cacheKey);
55+
out.writeBoolean(invalidateAll);
56+
}
57+
58+
@Override
59+
public ActionRequestValidationException validate() {
60+
if (!invalidateAll && packageId == null && cacheKey == null) {
61+
ActionRequestValidationException e = new ActionRequestValidationException();
62+
e.addValidationError("Either 'package_id', 'cache_key', or 'invalidate_all' must be specified");
63+
return e;
64+
}
65+
return null;
66+
}
67+
68+
public String getPackageId() {
69+
return packageId;
70+
}
71+
72+
public HunspellCacheInvalidateRequest setPackageId(String packageId) {
73+
this.packageId = packageId;
74+
return this;
75+
}
76+
77+
public String getLocale() {
78+
return locale;
79+
}
80+
81+
public HunspellCacheInvalidateRequest setLocale(String locale) {
82+
this.locale = locale;
83+
return this;
84+
}
85+
86+
public String getCacheKey() {
87+
return cacheKey;
88+
}
89+
90+
public HunspellCacheInvalidateRequest setCacheKey(String cacheKey) {
91+
this.cacheKey = cacheKey;
92+
return this;
93+
}
94+
95+
public boolean isInvalidateAll() {
96+
return invalidateAll;
97+
}
98+
99+
public HunspellCacheInvalidateRequest setInvalidateAll(boolean invalidateAll) {
100+
this.invalidateAll = invalidateAll;
101+
return this;
102+
}
103+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.action.admin.indices.cache.hunspell;
10+
11+
import org.opensearch.core.action.ActionResponse;
12+
import org.opensearch.core.common.io.stream.StreamInput;
13+
import org.opensearch.core.common.io.stream.StreamOutput;
14+
import org.opensearch.core.xcontent.ToXContentObject;
15+
import org.opensearch.core.xcontent.XContentBuilder;
16+
17+
import java.io.IOException;
18+
19+
/**
20+
* Response for Hunspell cache invalidation action.
21+
*
22+
* @opensearch.internal
23+
*/
24+
public class HunspellCacheInvalidateResponse extends ActionResponse implements ToXContentObject {
25+
26+
private final boolean acknowledged;
27+
private final int invalidatedCount;
28+
private final String packageId;
29+
private final String locale;
30+
private final String cacheKey;
31+
32+
public HunspellCacheInvalidateResponse(boolean acknowledged, int invalidatedCount, String packageId, String locale, String cacheKey) {
33+
this.acknowledged = acknowledged;
34+
this.invalidatedCount = invalidatedCount;
35+
this.packageId = packageId;
36+
this.locale = locale;
37+
this.cacheKey = cacheKey;
38+
}
39+
40+
public HunspellCacheInvalidateResponse(StreamInput in) throws IOException {
41+
super(in);
42+
this.acknowledged = in.readBoolean();
43+
this.invalidatedCount = in.readInt();
44+
this.packageId = in.readOptionalString();
45+
this.locale = in.readOptionalString();
46+
this.cacheKey = in.readOptionalString();
47+
}
48+
49+
@Override
50+
public void writeTo(StreamOutput out) throws IOException {
51+
out.writeBoolean(acknowledged);
52+
out.writeInt(invalidatedCount);
53+
out.writeOptionalString(packageId);
54+
out.writeOptionalString(locale);
55+
out.writeOptionalString(cacheKey);
56+
}
57+
58+
public boolean isAcknowledged() {
59+
return acknowledged;
60+
}
61+
62+
public int getInvalidatedCount() {
63+
return invalidatedCount;
64+
}
65+
66+
public String getPackageId() {
67+
return packageId;
68+
}
69+
70+
public String getLocale() {
71+
return locale;
72+
}
73+
74+
public String getCacheKey() {
75+
return cacheKey;
76+
}
77+
78+
@Override
79+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
80+
builder.startObject();
81+
builder.field("acknowledged", acknowledged);
82+
builder.field("invalidated_count", invalidatedCount);
83+
if (packageId != null) {
84+
builder.field("package_id", packageId);
85+
}
86+
if (locale != null) {
87+
builder.field("locale", locale);
88+
}
89+
if (cacheKey != null) {
90+
builder.field("cache_key", cacheKey);
91+
}
92+
builder.endObject();
93+
return builder;
94+
}
95+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.action.admin.indices.cache.hunspell;
10+
11+
import org.opensearch.action.support.ActionFilters;
12+
import org.opensearch.action.support.HandledTransportAction;
13+
import org.opensearch.common.inject.Inject;
14+
import org.opensearch.core.action.ActionListener;
15+
import org.opensearch.indices.analysis.HunspellService;
16+
import org.opensearch.tasks.Task;
17+
import org.opensearch.transport.TransportService;
18+
19+
/**
20+
* Transport action for Hunspell cache invalidation.
21+
*
22+
* This action is authorized via the "cluster:admin/hunspell/cache/clear" permission.
23+
* When the OpenSearch Security plugin is enabled, only users with cluster admin
24+
* permissions can execute this action.
25+
*
26+
* @opensearch.internal
27+
*/
28+
public class TransportHunspellCacheInvalidateAction extends HandledTransportAction<HunspellCacheInvalidateRequest, HunspellCacheInvalidateResponse> {
29+
30+
private final HunspellService hunspellService;
31+
32+
@Inject
33+
public TransportHunspellCacheInvalidateAction(
34+
TransportService transportService,
35+
ActionFilters actionFilters,
36+
HunspellService hunspellService
37+
) {
38+
super(HunspellCacheInvalidateAction.NAME, transportService, actionFilters, HunspellCacheInvalidateRequest::new);
39+
this.hunspellService = hunspellService;
40+
}
41+
42+
@Override
43+
protected void doExecute(Task task, HunspellCacheInvalidateRequest request, ActionListener<HunspellCacheInvalidateResponse> listener) {
44+
try {
45+
String packageId = request.getPackageId();
46+
String locale = request.getLocale();
47+
String cacheKey = request.getCacheKey();
48+
boolean invalidateAll = request.isInvalidateAll();
49+
50+
int invalidatedCount = 0;
51+
String responseCacheKey = null;
52+
53+
if (invalidateAll) {
54+
// Invalidate all cached dictionaries
55+
invalidatedCount = hunspellService.invalidateAllDictionaries();
56+
} else if (packageId != null && locale != null) {
57+
// Invalidate specific package + locale combination
58+
responseCacheKey = HunspellService.buildPackageCacheKey(packageId, locale);
59+
boolean invalidated = hunspellService.invalidateDictionary(responseCacheKey);
60+
invalidatedCount = invalidated ? 1 : 0;
61+
} else if (packageId != null) {
62+
// Invalidate all locales for a package
63+
invalidatedCount = hunspellService.invalidateDictionariesByPackage(packageId);
64+
} else if (cacheKey != null) {
65+
// Invalidate a specific cache key directly
66+
responseCacheKey = cacheKey;
67+
boolean invalidated = hunspellService.invalidateDictionary(cacheKey);
68+
invalidatedCount = invalidated ? 1 : 0;
69+
}
70+
71+
listener.onResponse(new HunspellCacheInvalidateResponse(
72+
true,
73+
invalidatedCount,
74+
packageId,
75+
locale,
76+
responseCacheKey
77+
));
78+
} catch (Exception e) {
79+
listener.onFailure(e);
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)