Skip to content

Commit 387c3c7

Browse files
tvernumjaymode
authored andcommitted
Introduce Application Privileges with support for Kibana RBAC (#32309)
This commit introduces "Application Privileges" to the X-Pack security model. Application Privileges are managed within Elasticsearch, and can be tested with the _has_privileges API, but do not grant access to any actions or resources within Elasticsearch. Their purpose is to allow applications outside of Elasticsearch to represent and store their own privileges model within Elasticsearch roles. Access to manage application privileges is handled in a new way that grants permission to specific application names only. This lays the foundation for more OLS on cluster privileges, which is implemented by allowing a cluster permission to inspect not just the action being executed, but also the request to which the action is applied. To support this, a "conditional cluster privilege" is introduced, which is like the existing cluster privilege, except that it has a Predicate over the request as well as over the action name. Specifically, this adds - GET/PUT/DELETE actions for defining application level privileges - application privileges in role definitions - application privileges in the has_privileges API - changes to the cluster permission class to support checking of request objects - a new "global" element on role definition to provide cluster object level security (only for manage application privileges) - changes to `kibana_user`, `kibana_dashboard_only_user` and `kibana_system` roles to use and manage application privileges Closes #29820 Closes #31559
1 parent e6b9f59 commit 387c3c7

File tree

93 files changed

+7171
-682
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+7171
-682
lines changed

server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import java.time.ZoneId;
6060
import java.time.ZonedDateTime;
6161
import java.util.ArrayList;
62+
import java.util.Collection;
6263
import java.util.Collections;
6364
import java.util.Date;
6465
import java.util.EnumSet;
@@ -931,8 +932,23 @@ public <T extends Streamable> List<T> readStreamableList(Supplier<T> constructor
931932
* Reads a list of objects
932933
*/
933934
public <T> List<T> readList(Writeable.Reader<T> reader) throws IOException {
935+
return readCollection(reader, ArrayList::new);
936+
}
937+
938+
/**
939+
* Reads a set of objects
940+
*/
941+
public <T> Set<T> readSet(Writeable.Reader<T> reader) throws IOException {
942+
return readCollection(reader, HashSet::new);
943+
}
944+
945+
/**
946+
* Reads a collection of objects
947+
*/
948+
private <T, C extends Collection<? super T>> C readCollection(Writeable.Reader<T> reader,
949+
IntFunction<C> constructor) throws IOException {
934950
int count = readArraySize();
935-
List<T> builder = new ArrayList<>(count);
951+
C builder = constructor.apply(count);
936952
for (int i=0; i<count; i++) {
937953
builder.add(reader.read(this));
938954
}

server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.nio.file.NoSuchFileException;
5656
import java.nio.file.NotDirectoryException;
5757
import java.time.ZonedDateTime;
58+
import java.util.Collection;
5859
import java.util.Collections;
5960
import java.util.Date;
6061
import java.util.EnumMap;
@@ -995,6 +996,16 @@ public void writeList(List<? extends Writeable> list) throws IOException {
995996
}
996997
}
997998

999+
/**
1000+
* Writes a collection of generic objects via a {@link Writer}
1001+
*/
1002+
public <T> void writeCollection(Collection<T> collection, Writer<T> writer) throws IOException {
1003+
writeVInt(collection.size());
1004+
for (T val: collection) {
1005+
writer.write(this, val);
1006+
}
1007+
}
1008+
9981009
/**
9991010
* Writes a list of strings
10001011
*/

server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.ArrayList;
3232
import java.util.Arrays;
3333
import java.util.Collections;
34+
import java.util.HashSet;
3435
import java.util.LinkedHashMap;
3536
import java.util.List;
3637
import java.util.Locale;
@@ -42,6 +43,7 @@
4243
import static org.hamcrest.Matchers.containsString;
4344
import static org.hamcrest.Matchers.equalTo;
4445
import static org.hamcrest.Matchers.hasToString;
46+
import static org.hamcrest.Matchers.iterableWithSize;
4547

4648
public class StreamTests extends ESTestCase {
4749

@@ -65,7 +67,7 @@ public void testBooleanSerialization() throws IOException {
6567
final Set<Byte> set = IntStream.range(Byte.MIN_VALUE, Byte.MAX_VALUE).mapToObj(v -> (byte) v).collect(Collectors.toSet());
6668
set.remove((byte) 0);
6769
set.remove((byte) 1);
68-
final byte[] corruptBytes = new byte[] { randomFrom(set) };
70+
final byte[] corruptBytes = new byte[]{randomFrom(set)};
6971
final BytesReference corrupt = new BytesArray(corruptBytes);
7072
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> corrupt.streamInput().readBoolean());
7173
final String message = String.format(Locale.ROOT, "unexpected byte [0x%02x]", corruptBytes[0]);
@@ -100,7 +102,7 @@ public void testOptionalBooleanSerialization() throws IOException {
100102
set.remove((byte) 0);
101103
set.remove((byte) 1);
102104
set.remove((byte) 2);
103-
final byte[] corruptBytes = new byte[] { randomFrom(set) };
105+
final byte[] corruptBytes = new byte[]{randomFrom(set)};
104106
final BytesReference corrupt = new BytesArray(corruptBytes);
105107
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> corrupt.streamInput().readOptionalBoolean());
106108
final String message = String.format(Locale.ROOT, "unexpected byte [0x%02x]", corruptBytes[0]);
@@ -119,22 +121,22 @@ public void testRandomVLongSerialization() throws IOException {
119121

120122
public void testSpecificVLongSerialization() throws IOException {
121123
List<Tuple<Long, byte[]>> values =
122-
Arrays.asList(
123-
new Tuple<>(0L, new byte[]{0}),
124-
new Tuple<>(-1L, new byte[]{1}),
125-
new Tuple<>(1L, new byte[]{2}),
126-
new Tuple<>(-2L, new byte[]{3}),
127-
new Tuple<>(2L, new byte[]{4}),
128-
new Tuple<>(Long.MIN_VALUE, new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1}),
129-
new Tuple<>(Long.MAX_VALUE, new byte[]{-2, -1, -1, -1, -1, -1, -1, -1, -1, 1})
130-
131-
);
124+
Arrays.asList(
125+
new Tuple<>(0L, new byte[]{0}),
126+
new Tuple<>(-1L, new byte[]{1}),
127+
new Tuple<>(1L, new byte[]{2}),
128+
new Tuple<>(-2L, new byte[]{3}),
129+
new Tuple<>(2L, new byte[]{4}),
130+
new Tuple<>(Long.MIN_VALUE, new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1}),
131+
new Tuple<>(Long.MAX_VALUE, new byte[]{-2, -1, -1, -1, -1, -1, -1, -1, -1, 1})
132+
133+
);
132134
for (Tuple<Long, byte[]> value : values) {
133135
BytesStreamOutput out = new BytesStreamOutput();
134136
out.writeZLong(value.v1());
135137
assertArrayEquals(Long.toString(value.v1()), value.v2(), BytesReference.toBytes(out.bytes()));
136138
BytesReference bytes = new BytesArray(value.v2());
137-
assertEquals(Arrays.toString(value.v2()), (long)value.v1(), bytes.streamInput().readZLong());
139+
assertEquals(Arrays.toString(value.v2()), (long) value.v1(), bytes.streamInput().readZLong());
138140
}
139141
}
140142

@@ -158,7 +160,7 @@ public void testLinkedHashMap() throws IOException {
158160
}
159161
BytesStreamOutput out = new BytesStreamOutput();
160162
out.writeGenericValue(write);
161-
LinkedHashMap<String, Integer> read = (LinkedHashMap<String, Integer>)out.bytes().streamInput().readGenericValue();
163+
LinkedHashMap<String, Integer> read = (LinkedHashMap<String, Integer>) out.bytes().streamInput().readGenericValue();
162164
assertEquals(size, read.size());
163165
int index = 0;
164166
for (Map.Entry<String, Integer> entry : read.entrySet()) {
@@ -172,7 +174,8 @@ public void testFilterStreamInputDelegatesAvailable() throws IOException {
172174
final int length = randomIntBetween(1, 1024);
173175
StreamInput delegate = StreamInput.wrap(new byte[length]);
174176

175-
FilterStreamInput filterInputStream = new FilterStreamInput(delegate) {};
177+
FilterStreamInput filterInputStream = new FilterStreamInput(delegate) {
178+
};
176179
assertEquals(filterInputStream.available(), length);
177180

178181
// read some bytes
@@ -201,7 +204,7 @@ public void testReadArraySize() throws IOException {
201204
}
202205
stream.writeByteArray(array);
203206
InputStreamStreamInput streamInput = new InputStreamStreamInput(StreamInput.wrap(BytesReference.toBytes(stream.bytes())), array
204-
.length-1);
207+
.length - 1);
205208
expectThrows(EOFException.class, streamInput::readByteArray);
206209
streamInput = new InputStreamStreamInput(StreamInput.wrap(BytesReference.toBytes(stream.bytes())), BytesReference.toBytes(stream
207210
.bytes()).length);
@@ -230,6 +233,21 @@ public void testWritableArrays() throws IOException {
230233
assertThat(targetArray, equalTo(sourceArray));
231234
}
232235

236+
public void testSetOfLongs() throws IOException {
237+
final int size = randomIntBetween(0, 6);
238+
final Set<Long> sourceSet = new HashSet<>(size);
239+
for (int i = 0; i < size; i++) {
240+
sourceSet.add(randomLongBetween(i * 1000, (i + 1) * 1000 - 1));
241+
}
242+
assertThat(sourceSet, iterableWithSize(size));
243+
244+
final BytesStreamOutput out = new BytesStreamOutput();
245+
out.writeCollection(sourceSet, StreamOutput::writeLong);
246+
247+
final Set<Long> targetSet = out.bytes().streamInput().readSet(StreamInput::readLong);
248+
assertThat(targetSet, equalTo(sourceSet));
249+
}
250+
233251
static final class WriteableString implements Writeable {
234252
final String string;
235253

test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
import java.util.concurrent.atomic.AtomicInteger;
150150
import java.util.function.BooleanSupplier;
151151
import java.util.function.Consumer;
152+
import java.util.function.IntFunction;
152153
import java.util.function.Predicate;
153154
import java.util.function.Supplier;
154155
import java.util.stream.Collectors;
@@ -717,6 +718,20 @@ public static String[] generateRandomStringArray(int maxArraySize, int stringSiz
717718
return generateRandomStringArray(maxArraySize, stringSize, allowNull, true);
718719
}
719720

721+
public static <T> T[] randomArray(int maxArraySize, IntFunction<T[]> arrayConstructor, Supplier<T> valueConstructor) {
722+
return randomArray(0, maxArraySize, arrayConstructor, valueConstructor);
723+
}
724+
725+
public static <T> T[] randomArray(int minArraySize, int maxArraySize, IntFunction<T[]> arrayConstructor, Supplier<T> valueConstructor) {
726+
final int size = randomIntBetween(minArraySize, maxArraySize);
727+
final T[] array = arrayConstructor.apply(size);
728+
for (int i = 0; i < array.length; i++) {
729+
array[i] = valueConstructor.get();
730+
}
731+
return array;
732+
}
733+
734+
720735
private static final String[] TIME_SUFFIXES = new String[]{"d", "h", "ms", "s", "m", "micros", "nanos"};
721736

722737
public static String randomTimeValue(int lower, int upper, String... suffixes) {

x-pack/docs/en/rest-api/security/privileges.asciidoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ The following example output indicates which privileges the "rdeniro" user has:
8484
"read" : true,
8585
"write" : false
8686
}
87-
}
87+
},
88+
"application" : {}
8889
}
8990
--------------------------------------------------
9091
// TESTRESPONSE[s/"rdeniro"/"$body.username"/]

x-pack/docs/en/rest-api/security/roles.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ role. If the role is not defined in the `native` realm, the request 404s.
140140
},
141141
"query" : "{\"match\": {\"title\": \"foo\"}}"
142142
} ],
143+
"applications" : [ ],
143144
"run_as" : [ "other_user" ],
144145
"metadata" : {
145146
"version" : 1

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@
133133
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExceptExpression;
134134
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression;
135135
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
136+
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges;
137+
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
136138
import org.elasticsearch.xpack.core.security.transport.netty4.SecurityNetty4Transport;
137139
import org.elasticsearch.xpack.core.ssl.SSLService;
138140
import org.elasticsearch.xpack.core.ssl.action.GetCertificateInfoAction;
@@ -342,6 +344,11 @@ public List<NamedWriteableRegistry.Entry> getNamedWriteables() {
342344
new NamedWriteableRegistry.Entry(ClusterState.Custom.class, TokenMetaData.TYPE, TokenMetaData::new),
343345
new NamedWriteableRegistry.Entry(NamedDiff.class, TokenMetaData.TYPE, TokenMetaData::readDiffFrom),
344346
new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.SECURITY, SecurityFeatureSetUsage::new),
347+
// security : conditional privileges
348+
new NamedWriteableRegistry.Entry(ConditionalClusterPrivilege.class,
349+
ConditionalClusterPrivileges.ManageApplicationPrivileges.WRITEABLE_NAME,
350+
ConditionalClusterPrivileges.ManageApplicationPrivileges::createFrom),
351+
// security : role-mappings
345352
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AllExpression.NAME, AllExpression::new),
346353
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AnyExpression.NAME, AnyExpression::new),
347354
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, FieldExpression.NAME, FieldExpression::new),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action.privilege;
8+
9+
import java.util.Collection;
10+
11+
/**
12+
* Interface implemented by all Requests that manage application privileges
13+
*/
14+
public interface ApplicationPrivilegesRequest {
15+
16+
Collection<String> getApplicationNames();
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.action.privilege;
7+
8+
import org.elasticsearch.action.Action;
9+
10+
/**
11+
* Action for deleting application privileges.
12+
*/
13+
public final class DeletePrivilegesAction extends Action<DeletePrivilegesResponse> {
14+
15+
public static final DeletePrivilegesAction INSTANCE = new DeletePrivilegesAction();
16+
public static final String NAME = "cluster:admin/xpack/security/privilege/delete";
17+
18+
private DeletePrivilegesAction() {
19+
super(NAME);
20+
}
21+
22+
@Override
23+
public DeletePrivilegesResponse newResponse() {
24+
return new DeletePrivilegesResponse();
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.action.privilege;
7+
8+
import org.elasticsearch.action.ActionRequest;
9+
import org.elasticsearch.action.ActionRequestValidationException;
10+
import org.elasticsearch.action.support.WriteRequest;
11+
import org.elasticsearch.common.Strings;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.common.io.stream.StreamOutput;
14+
15+
import java.io.IOException;
16+
import java.util.Arrays;
17+
import java.util.Collection;
18+
import java.util.Collections;
19+
20+
import static org.elasticsearch.action.ValidateActions.addValidationError;
21+
22+
/**
23+
* A request to delete an application privilege.
24+
*/
25+
public final class DeletePrivilegesRequest extends ActionRequest
26+
implements ApplicationPrivilegesRequest, WriteRequest<DeletePrivilegesRequest> {
27+
28+
private String application;
29+
private String[] privileges;
30+
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
31+
32+
public DeletePrivilegesRequest() {
33+
this(null, Strings.EMPTY_ARRAY);
34+
}
35+
36+
public DeletePrivilegesRequest(String application, String[] privileges) {
37+
this.application = application;
38+
this.privileges = privileges;
39+
}
40+
41+
@Override
42+
public DeletePrivilegesRequest setRefreshPolicy(RefreshPolicy refreshPolicy) {
43+
this.refreshPolicy = refreshPolicy;
44+
return this;
45+
}
46+
47+
@Override
48+
public RefreshPolicy getRefreshPolicy() {
49+
return refreshPolicy;
50+
}
51+
52+
@Override
53+
public ActionRequestValidationException validate() {
54+
ActionRequestValidationException validationException = null;
55+
if (Strings.isNullOrEmpty(application)) {
56+
validationException = addValidationError("application name is missing", validationException);
57+
}
58+
if (privileges == null || privileges.length == 0 || Arrays.stream(privileges).allMatch(Strings::isNullOrEmpty)) {
59+
validationException = addValidationError("privileges are missing", validationException);
60+
}
61+
return validationException;
62+
}
63+
64+
public void application(String application) {
65+
this.application = application;
66+
}
67+
68+
public String application() {
69+
return application;
70+
}
71+
72+
@Override
73+
public Collection<String> getApplicationNames() {
74+
return Collections.singleton(application);
75+
}
76+
77+
public String[] privileges() {
78+
return this.privileges;
79+
}
80+
81+
public void privileges(String[] privileges) {
82+
this.privileges = privileges;
83+
}
84+
85+
@Override
86+
public void readFrom(StreamInput in) throws IOException {
87+
super.readFrom(in);
88+
application = in.readString();
89+
privileges = in.readStringArray();
90+
refreshPolicy = RefreshPolicy.readFrom(in);
91+
}
92+
93+
@Override
94+
public void writeTo(StreamOutput out) throws IOException {
95+
super.writeTo(out);
96+
out.writeString(application);
97+
out.writeStringArray(privileges);
98+
refreshPolicy.writeTo(out);
99+
}
100+
101+
}

0 commit comments

Comments
 (0)