-
Notifications
You must be signed in to change notification settings - Fork 282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Api token authc/z implementation with Cache #4992
base: feature/api-tokens
Are you sure you want to change the base?
Changes from 10 commits
9f695fa
d3fcc4a
6904317
17bca93
92d4e60
22cfbe8
665b9e9
e39df0d
ad63974
73eb2ab
6418226
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.security.action.apitokens; | ||
|
||
import java.io.IOException; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
|
||
import org.opensearch.common.xcontent.LoggingDeprecationHandler; | ||
import org.opensearch.common.xcontent.XContentType; | ||
import org.opensearch.core.common.bytes.BytesReference; | ||
import org.opensearch.core.index.shard.ShardId; | ||
import org.opensearch.core.xcontent.NamedXContentRegistry; | ||
import org.opensearch.core.xcontent.XContentParser; | ||
import org.opensearch.index.engine.Engine; | ||
import org.opensearch.index.shard.IndexingOperationListener; | ||
|
||
/** | ||
* This class implements an index operation listener for operations performed on api tokens | ||
* These indices are defined on bootstrap and configured to listen in OpenSearchSecurityPlugin.java | ||
*/ | ||
public class ApiTokenIndexListenerCache implements IndexingOperationListener { | ||
|
||
private final static Logger log = LogManager.getLogger(ApiTokenIndexListenerCache.class); | ||
|
||
private static final ApiTokenIndexListenerCache INSTANCE = new ApiTokenIndexListenerCache(); | ||
private final ConcurrentHashMap<String, String> idToJtiMap = new ConcurrentHashMap<>(); | ||
|
||
private Map<String, Permissions> jtis = new ConcurrentHashMap<>(); | ||
|
||
private boolean initialized; | ||
|
||
private ApiTokenIndexListenerCache() {} | ||
|
||
public static ApiTokenIndexListenerCache getInstance() { | ||
return ApiTokenIndexListenerCache.INSTANCE; | ||
} | ||
|
||
/** | ||
* Initializes the ApiTokenIndexListenerCache. | ||
* This method is called during the plugin's initialization process. | ||
* | ||
*/ | ||
public void initialize() { | ||
|
||
if (initialized) { | ||
return; | ||
Check warning on line 56 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L56
|
||
} | ||
|
||
initialized = true; | ||
Check warning on line 59 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L59
|
||
|
||
} | ||
Check warning on line 61 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L61
|
||
|
||
public boolean isInitialized() { | ||
return initialized; | ||
Check warning on line 64 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L64
|
||
} | ||
|
||
/** | ||
* This method is called after an index operation is performed. | ||
* It adds the JTI of the indexed document to the cache and maps the document ID to the JTI (for deletion handling). | ||
* @param shardId The shard ID of the index where the operation was performed. | ||
* @param index The index where the operation was performed. | ||
* @param result The result of the index operation. | ||
*/ | ||
@Override | ||
public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { | ||
BytesReference sourceRef = index.source(); | ||
Check warning on line 76 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L76
|
||
|
||
try { | ||
XContentParser parser = XContentType.JSON.xContent() | ||
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, sourceRef.streamInput()); | ||
Check warning on line 80 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L79-L80
|
||
|
||
ApiToken token = ApiToken.fromXContent(parser); | ||
jtis.put(token.getJti(), new Permissions(token.getClusterPermissions(), token.getIndexPermissions())); | ||
idToJtiMap.put(index.id(), token.getJti()); | ||
Check warning on line 84 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L82-L84
|
||
|
||
} catch (IOException e) { | ||
log.error("Failed to parse indexed document", e); | ||
} | ||
} | ||
Check warning on line 89 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L86-L89
|
||
|
||
/** | ||
* This method is called after a delete operation is performed. | ||
* It deletes the corresponding document id in the map and the corresponding jti from the cache. | ||
* @param shardId The shard ID of the index where the delete operation was performed. | ||
* @param delete The delete operation that was performed. | ||
* @param result The result of the delete operation. | ||
*/ | ||
@Override | ||
public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { | ||
String docId = delete.id(); | ||
String jti = idToJtiMap.remove(docId); | ||
Check warning on line 101 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L100-L101
|
||
if (jti != null) { | ||
jtis.remove(jti); | ||
log.debug("Removed token with ID {} and JTI {} from cache", docId, jti); | ||
Check warning on line 104 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L103-L104
|
||
} | ||
} | ||
Check warning on line 106 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java Codecov / codecov/patchsrc/main/java/org/opensearch/security/action/apitokens/ApiTokenIndexListenerCache.java#L106
|
||
|
||
public Map<String, Permissions> getJtis() { | ||
return jtis; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.security.action.apitokens; | ||
|
||
import java.util.List; | ||
|
||
public class Permissions { | ||
private List<String> clusterPerm; | ||
private List<ApiToken.IndexPermission> indexPermission; | ||
|
||
// Constructor | ||
public Permissions(List<String> clusterPerm, List<ApiToken.IndexPermission> indexPermission) { | ||
this.clusterPerm = clusterPerm; | ||
this.indexPermission = indexPermission; | ||
} | ||
|
||
// Getters and setters | ||
public List<String> getClusterPerm() { | ||
return clusterPerm; | ||
} | ||
|
||
public void setClusterPerm(List<String> clusterPerm) { | ||
this.clusterPerm = clusterPerm; | ||
} | ||
|
||
public List<ApiToken.IndexPermission> getIndexPermission() { | ||
return indexPermission; | ||
} | ||
|
||
public void setIndexPermission(List<ApiToken.IndexPermission> indexPermission) { | ||
this.indexPermission = indexPermission; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -584,22 +584,24 @@ | |
originalSource = "{}"; | ||
} | ||
if (securityIndicesMatcher.test(shardId.getIndexName())) { | ||
try ( | ||
XContentParser parser = XContentHelper.createParser( | ||
NamedXContentRegistry.EMPTY, | ||
THROW_UNSUPPORTED_OPERATION, | ||
originalResult.internalSourceRef(), | ||
XContentType.JSON | ||
) | ||
) { | ||
Object base64 = parser.map().values().iterator().next(); | ||
if (base64 instanceof String) { | ||
originalSource = (new String(BaseEncoding.base64().decode((String) base64), StandardCharsets.UTF_8)); | ||
} else { | ||
originalSource = XContentHelper.convertToJson(originalResult.internalSourceRef(), false, XContentType.JSON); | ||
if (originalSource == null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes in this file are to correct a mis-merge that happened in prior PRs to this feature branch. |
||
try ( | ||
XContentParser parser = XContentHelper.createParser( | ||
NamedXContentRegistry.EMPTY, | ||
THROW_UNSUPPORTED_OPERATION, | ||
originalResult.internalSourceRef(), | ||
XContentType.JSON | ||
) | ||
) { | ||
Object base64 = parser.map().values().iterator().next(); | ||
if (base64 instanceof String) { | ||
originalSource = (new String(BaseEncoding.base64().decode((String) base64), StandardCharsets.UTF_8)); | ||
} else { | ||
originalSource = XContentHelper.convertToJson(originalResult.internalSourceRef(), false, XContentType.JSON); | ||
} | ||
} catch (Exception e) { | ||
log.error(e.toString()); | ||
} | ||
} catch (Exception e) { | ||
log.error(e.toString()); | ||
} | ||
|
||
try ( | ||
|
@@ -640,7 +642,7 @@ | |
} | ||
} | ||
|
||
if (!complianceConfig.shouldLogWriteMetadataOnly()) { | ||
if (!complianceConfig.shouldLogWriteMetadataOnly() && !complianceConfig.shouldLogDiffsForWrite()) { | ||
if (securityIndicesMatcher.test(shardId.getIndexName())) { | ||
// current source, normally not null or empty | ||
try ( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious, why use this approach w/ IndexOperationListener opposed to the same approach as the security index vs in-memory data-structures that back the security index?
i.e. With the security index, if I call the API to create a new user. The node that receives the request will fulfill the request and add the new user to the security index. After updating the security index, it will use an internal transport action (TransportConfigUpdateAction) to instruct all nodes of the cluster to re-read from the security index. On node bootstrap, each node of the cluster reads from the security index and populates their in-memory cache of the security index.