-
Notifications
You must be signed in to change notification settings - Fork 25k
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
Add cache for application privileges #55836
Changes from 9 commits
b72d0d3
a02aba3
6d9fa0a
ee33fff
4bbd16d
df28676
4a63a16
a1d177c
05061f9
daafb80
545e27c
5987b3b
6094178
1c58309
9100c24
3f9c5b7
08865dd
b9538a8
9c5e947
f5b166c
1e7ce7c
f1f3e72
a7b5b40
5306bc1
03680bc
cdfe886
435993d
20be80d
d2f2f3e
f1f7035
db6020e
73dda1f
ea9172f
65f9524
04bb476
07b7837
34464d7
e87d897
5acc7ec
5e3174a
385fb12
e411dc9
58b3942
a8f8e81
9a763c3
a43b3ef
ac6c39f
8e1dc13
21c89fb
1499a26
3b7e773
83a0e19
3deef1e
9364e36
0280c3e
ae3c784
68a4dc1
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,19 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.core.security.action.privilege; | ||
|
||
import org.elasticsearch.action.ActionType; | ||
|
||
public class ClearPrivilegesCacheAction extends ActionType<ClearPrivilegesCacheResponse> { | ||
|
||
public static final ClearPrivilegesCacheAction INSTANCE = new ClearPrivilegesCacheAction(); | ||
public static final String NAME = "cluster:admin/xpack/security/privilege/cache/clear"; | ||
|
||
protected ClearPrivilegesCacheAction() { | ||
super(NAME, ClearPrivilegesCacheResponse::new); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.core.security.action.privilege; | ||
|
||
import org.elasticsearch.action.support.nodes.BaseNodesRequest; | ||
import org.elasticsearch.common.io.stream.StreamInput; | ||
import org.elasticsearch.common.io.stream.StreamOutput; | ||
import org.elasticsearch.transport.TransportRequest; | ||
|
||
import java.io.IOException; | ||
|
||
public class ClearPrivilegesCacheRequest extends BaseNodesRequest<ClearPrivilegesCacheRequest> { | ||
|
||
String[] applicationNames; | ||
|
||
public ClearPrivilegesCacheRequest() { | ||
super((String[]) null); | ||
} | ||
|
||
public ClearPrivilegesCacheRequest(StreamInput in) throws IOException { | ||
super(in); | ||
applicationNames = in.readOptionalStringArray(); | ||
} | ||
|
||
public ClearPrivilegesCacheRequest applicationNames(String... applicationNames) { | ||
this.applicationNames = applicationNames; | ||
return this; | ||
} | ||
|
||
public String[] applicationNames() { | ||
return applicationNames; | ||
} | ||
|
||
@Override | ||
public void writeTo(StreamOutput out) throws IOException { | ||
super.writeTo(out); | ||
out.writeOptionalStringArray(applicationNames); | ||
} | ||
|
||
public static class Node extends TransportRequest { | ||
private String[] applicationNames; | ||
|
||
public Node(StreamInput in) throws IOException { | ||
super(in); | ||
applicationNames = in.readOptionalStringArray(); | ||
} | ||
|
||
public Node(ClearPrivilegesCacheRequest request) { | ||
this.applicationNames = request.applicationNames(); | ||
} | ||
|
||
public String[] getApplicationNames() { | ||
return applicationNames; | ||
} | ||
|
||
@Override | ||
public void writeTo(StreamOutput out) throws IOException { | ||
super.writeTo(out); | ||
out.writeOptionalStringArray(applicationNames); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.core.security.action.privilege; | ||
|
||
import org.elasticsearch.action.FailedNodeException; | ||
import org.elasticsearch.action.support.nodes.BaseNodeResponse; | ||
import org.elasticsearch.action.support.nodes.BaseNodesResponse; | ||
import org.elasticsearch.cluster.ClusterName; | ||
import org.elasticsearch.cluster.node.DiscoveryNode; | ||
import org.elasticsearch.common.io.stream.StreamInput; | ||
import org.elasticsearch.common.io.stream.StreamOutput; | ||
import org.elasticsearch.common.xcontent.ToXContentFragment; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
|
||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
public class ClearPrivilegesCacheResponse extends BaseNodesResponse<ClearPrivilegesCacheResponse.Node> | ||
implements ToXContentFragment { | ||
|
||
public ClearPrivilegesCacheResponse(StreamInput in) throws IOException { | ||
super(in); | ||
} | ||
|
||
public ClearPrivilegesCacheResponse(ClusterName clusterName, List<Node> nodes, List<FailedNodeException> failures) { | ||
super(clusterName, nodes, failures); | ||
} | ||
|
||
@Override | ||
protected List<Node> readNodesFrom(StreamInput in) throws IOException { | ||
return in.readList(Node::new); | ||
} | ||
|
||
@Override | ||
protected void writeNodesTo(StreamOutput out, List<Node> nodes) throws IOException { | ||
out.writeList(nodes); | ||
} | ||
|
||
@Override | ||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { | ||
builder.startObject("nodes"); | ||
for (Node node : getNodes()) { | ||
builder.startObject(node.getNode().getId()); | ||
builder.field("name", node.getNode().getName()); | ||
builder.endObject(); | ||
} | ||
builder.endObject(); | ||
return builder; | ||
} | ||
|
||
public static class Node extends BaseNodeResponse { | ||
public Node(StreamInput in) throws IOException { | ||
super(in); | ||
} | ||
|
||
public Node(DiscoveryNode node) { | ||
super(node); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.security.action.privilege; | ||
|
||
import org.elasticsearch.action.FailedNodeException; | ||
import org.elasticsearch.action.support.ActionFilters; | ||
import org.elasticsearch.action.support.nodes.TransportNodesAction; | ||
import org.elasticsearch.cluster.service.ClusterService; | ||
import org.elasticsearch.common.inject.Inject; | ||
import org.elasticsearch.common.io.stream.StreamInput; | ||
import org.elasticsearch.tasks.Task; | ||
import org.elasticsearch.threadpool.ThreadPool; | ||
import org.elasticsearch.transport.TransportService; | ||
import org.elasticsearch.xpack.core.security.action.privilege.ClearPrivilegesCacheAction; | ||
import org.elasticsearch.xpack.core.security.action.privilege.ClearPrivilegesCacheRequest; | ||
import org.elasticsearch.xpack.core.security.action.privilege.ClearPrivilegesCacheResponse; | ||
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; | ||
import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; | ||
|
||
import java.io.IOException; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
public class TransportClearPrivilegesCacheAction | ||
extends TransportNodesAction<ClearPrivilegesCacheRequest, ClearPrivilegesCacheResponse, ClearPrivilegesCacheRequest.Node, ClearPrivilegesCacheResponse.Node> { | ||
|
||
private final NativePrivilegeStore privilegesStore; | ||
private final CompositeRolesStore rolesStore; | ||
|
||
@Inject | ||
public TransportClearPrivilegesCacheAction( | ||
ThreadPool threadPool, | ||
ClusterService clusterService, | ||
TransportService transportService, | ||
ActionFilters actionFilters, | ||
NativePrivilegeStore privilegesStore, | ||
CompositeRolesStore rolesStore) { | ||
super( | ||
ClearPrivilegesCacheAction.NAME, | ||
threadPool, | ||
clusterService, | ||
transportService, | ||
actionFilters, | ||
ClearPrivilegesCacheRequest::new, | ||
ClearPrivilegesCacheRequest.Node::new, | ||
ThreadPool.Names.MANAGEMENT, | ||
ClearPrivilegesCacheResponse.Node.class); | ||
this.privilegesStore = privilegesStore; | ||
this.rolesStore = rolesStore; | ||
} | ||
|
||
@Override | ||
protected ClearPrivilegesCacheResponse newResponse( | ||
ClearPrivilegesCacheRequest request, List<ClearPrivilegesCacheResponse.Node> nodes, List<FailedNodeException> failures) { | ||
return new ClearPrivilegesCacheResponse(clusterService.getClusterName(), nodes, failures); | ||
} | ||
|
||
@Override | ||
protected ClearPrivilegesCacheRequest.Node newNodeRequest(ClearPrivilegesCacheRequest request) { | ||
return new ClearPrivilegesCacheRequest.Node(request); | ||
} | ||
|
||
@Override | ||
protected ClearPrivilegesCacheResponse.Node newNodeResponse(StreamInput in) throws IOException { | ||
return new ClearPrivilegesCacheResponse.Node(in); | ||
} | ||
|
||
@Override | ||
protected ClearPrivilegesCacheResponse.Node nodeOperation(ClearPrivilegesCacheRequest.Node request, Task task) { | ||
if (request.getApplicationNames() == null || request.getApplicationNames().length == 0) { | ||
privilegesStore.invalidateAll(); | ||
} else { | ||
privilegesStore.invalidate(Arrays.asList(request.getApplicationNames())); | ||
ywangd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
rolesStore.invalidateAll(); | ||
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. I'm not sure about this. It seems like this API ends up doing something other than what it was supposed to, just because we assume that the caller wants it. 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. From just the API point of view, you are right that these two should not be tied together. There are valid use cases when user only wants to actively clear privileges cache. I did this because the two are always tied together in I tried to avoid nested callbacks (clear role cache then clear privileges cache). It seems OK and more efficient by just looking at I could either just go with nested callback or create a transport layer only action to clear both caches. So it is not exposed at REST layer and still has the efficiency on transport layer. But this does lead some code redundancy. Another option is to have a query parameter for the clear privilege cache API. When set to true, it clears both caches. What do you think? |
||
return new ClearPrivilegesCacheResponse.Node(clusterService.localNode()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -422,6 +422,7 @@ public static void buildRoleFromDescriptors(Collection<RoleDescriptor> roleDescr | |
final Set<String> applicationPrivilegeNames = applicationPrivilegesMap.values().stream() | ||
.flatMap(Collection::stream) | ||
.collect(Collectors.toSet()); | ||
// Role itself is cached, so skipping caching for application privileges | ||
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. I don't follow this comment. 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. Good catch! This comment is obsolete and should be removed. It is for the initial iteration where I tried to separate this usages from others. |
||
privilegeStore.getPrivileges(applicationNames, applicationPrivilegeNames, ActionListener.wrap(appPrivileges -> { | ||
applicationPrivilegesMap.forEach((key, names) -> ApplicationPrivilege.get(key.v1(), names, appPrivileges) | ||
.forEach(priv -> builder.addApplicationPrivilege(priv, key.v2()))); | ||
|
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.
Separate to this PR, it feels like we could consolidate these duplicate classes into a common base class.
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.
A common base class for all
ClearXxxCacheResponse
?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.
Yes. Not a priority, but there's a bunch of copy paste here that we could ditch.