Skip to content

Commit 4c6bb42

Browse files
committed
Refactor CompositeRolesStore for speration of concerns
The process of role resolving is to build the Role object from an Authentication object. The high level steps of this process is as the follows: 1. Locate the role reference for the Authentication, e.g. for regular user, this means a collection of role names. 2. Retrieve the role descriptors for the role reference, e.g. search the security index to get the role descriptors for the role name. 3. Build the Role object based on the role descriptors. There are also special cases in the above process. For example, API keys do not have role names, but two byteReference representing the role descriptors. API keys also have a nested Role structure for limiting the key's actual privileges based on the owner's. Today, this process is managed by a single CompositeRolesStore class and the steps are not clearly separated. This PR improves the situation by introducing a new RoleReferenceResolver class that is responsible for turning roleReference into role descriptors. CompositeRolesStore is now only responsible for buiding the Role from the descriptors. The RoleReference is also a new concept introduced by this PR, along with AuthenticationContext and Subject. They technically belong to the issue of revisiting Authentication class (#80117). But they are needed here to faciliate the changes. Their usage will be expanded once we start working on #80117. A Subject knows how to return a list of RoleReference and the final Role is the intersection of all the RoleReference. This was a concept for API keys. It is now generalised in this PR in the light on potential expanded usage of limitedBy roles. Relates: #80117
1 parent 591f551 commit 4c6bb42

File tree

20 files changed

+1286
-666
lines changed

20 files changed

+1286
-666
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.authc;
9+
10+
import org.elasticsearch.Version;
11+
import org.elasticsearch.xpack.core.security.user.User;
12+
13+
import java.util.Map;
14+
15+
import static org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
16+
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
17+
18+
public class AuthenticationContext {
19+
20+
private final Version version;
21+
private final Subject authenticatingSubject;
22+
private final Subject effectiveSubject;
23+
// TODO: Rename to AuthenticationMethod
24+
private final AuthenticationType type;
25+
26+
private AuthenticationContext(
27+
Version version,
28+
Subject authenticatingSubject,
29+
Subject effectiveSubject,
30+
AuthenticationType authenticationType
31+
) {
32+
this.version = version;
33+
this.authenticatingSubject = authenticatingSubject;
34+
this.effectiveSubject = effectiveSubject;
35+
this.type = authenticationType;
36+
}
37+
38+
public boolean isRunAs() {
39+
assert authenticatingSubject != null && effectiveSubject != null;
40+
return authenticatingSubject != effectiveSubject;
41+
}
42+
43+
public Subject getAuthenticatingSubject() {
44+
return authenticatingSubject;
45+
}
46+
47+
public Subject getEffectiveSubject() {
48+
return effectiveSubject;
49+
}
50+
51+
public Authentication toAuthentication() {
52+
return new Authentication(
53+
effectiveSubject.getUser(),
54+
authenticatingSubject.getRealm(),
55+
effectiveSubject.getRealm(),
56+
version,
57+
type,
58+
authenticatingSubject.getMetadata()
59+
);
60+
}
61+
62+
public static AuthenticationContext fromAuthentication(Authentication authentication) {
63+
final Builder builder = new Builder(authentication.getVersion());
64+
builder.authenticationType(authentication.getAuthenticationType());
65+
final User user = authentication.getUser();
66+
if (user.isRunAs()) {
67+
builder.authenticatingSubject(user.authenticatedUser(), authentication.getAuthenticatedBy(), authentication.getMetadata());
68+
// The lookup user for run-as currently don't have authentication metadata associated with them because
69+
// lookupUser only returns the User object. The lookup user for authorization delegation does have
70+
// authentication metadata, but the realm does not expose this difference between authenticatingUser and
71+
// delegateUser so effectively this is handled together with the authenticatingSubject not effectiveSubject.
72+
builder.effectiveSubject(user, authentication.getLookedUpBy(), Map.of());
73+
} else {
74+
builder.authenticatingSubject(user, authentication.getAuthenticatedBy(), authentication.getMetadata());
75+
}
76+
return builder.build();
77+
}
78+
79+
public static class Builder {
80+
private final Version version;
81+
private AuthenticationType authenticationType;
82+
private Subject authenticatingSubject;
83+
private Subject effectiveSubject;
84+
85+
public Builder() {
86+
this(Version.CURRENT);
87+
}
88+
89+
public Builder(Version version) {
90+
this.version = version;
91+
}
92+
93+
public Builder authenticationType(AuthenticationType authenticationType) {
94+
this.authenticationType = authenticationType;
95+
return this;
96+
}
97+
98+
public Builder authenticatingSubject(User authenticatingUser, RealmRef authenticatingRealmRef, Map<String, Object> metadata) {
99+
this.authenticatingSubject = new Subject(authenticatingUser, authenticatingRealmRef, version, metadata);
100+
return this;
101+
}
102+
103+
public Builder effectiveSubject(User effectiveUser, RealmRef lookupRealmRef, Map<String, Object> metadata) {
104+
this.effectiveSubject = new Subject(effectiveUser, lookupRealmRef, version, metadata);
105+
return this;
106+
}
107+
108+
public AuthenticationContext build() {
109+
if (effectiveSubject == null) {
110+
effectiveSubject = authenticatingSubject;
111+
}
112+
return new AuthenticationContext(version, authenticatingSubject, effectiveSubject, authenticationType);
113+
}
114+
}
115+
116+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ public final class AuthenticationField {
2424
public static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors";
2525
public static final String API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY = "_security_api_key_limited_by_role_descriptors";
2626

27+
public static final String ANONYMOUS_REALM_NAME = "__anonymous";
28+
public static final String ANONYMOUS_REALM_TYPE = "__anonymous";
29+
2730
private AuthenticationField() {}
2831
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.authc;
9+
10+
import org.elasticsearch.ElasticsearchSecurityException;
11+
import org.elasticsearch.Version;
12+
import org.elasticsearch.common.bytes.BytesArray;
13+
import org.elasticsearch.common.bytes.BytesReference;
14+
import org.elasticsearch.common.util.ArrayUtils;
15+
import org.elasticsearch.core.Nullable;
16+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
17+
import org.elasticsearch.xpack.core.security.authz.store.RoleReference;
18+
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
19+
import org.elasticsearch.xpack.core.security.user.User;
20+
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES;
25+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY;
26+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY;
27+
28+
/**
29+
* A subject is a more generic concept similar to user and associated to the current authentication.
30+
* It is more generic than user because it can also represent API keys and service accounts.
31+
* It also contains authentication level information, e.g. realm and metadata so that it can answer
32+
* queries in a better encapsulated way.
33+
*/
34+
public class Subject {
35+
36+
public enum Type {
37+
USER,
38+
API_KEY,
39+
SERVICE_ACCOUNT,
40+
}
41+
42+
private final Version version;
43+
private final User user;
44+
private final Authentication.RealmRef realm;
45+
private final Type type;
46+
private final Map<String, Object> metadata;
47+
48+
public Subject(User user, Authentication.RealmRef realm) {
49+
this(user, realm, Version.CURRENT, Map.of());
50+
}
51+
52+
public Subject(User user, Authentication.RealmRef realm, Version version, Map<String, Object> metadata) {
53+
this.version = version;
54+
this.user = user;
55+
this.realm = realm;
56+
// Realm can be null for run-as user if it does not exist. Pretend it is a user and it will be rejected later in authorization
57+
// This is to be consistent with existing behaviour.
58+
if (realm == null) {
59+
this.type = Type.USER;
60+
} else if (AuthenticationField.API_KEY_REALM_TYPE.equals(realm.getType())) {
61+
assert AuthenticationField.API_KEY_REALM_NAME.equals(realm.getName()) : "api key realm name mismatch";
62+
this.type = Type.API_KEY;
63+
} else if (ServiceAccountSettings.REALM_TYPE.equals(realm.getType())) {
64+
assert ServiceAccountSettings.REALM_NAME.equals(realm.getName()) : "service account realm name mismatch";
65+
this.type = Type.SERVICE_ACCOUNT;
66+
} else {
67+
this.type = Type.USER;
68+
}
69+
this.metadata = metadata;
70+
}
71+
72+
public User getUser() {
73+
return user;
74+
}
75+
76+
public Authentication.RealmRef getRealm() {
77+
return realm;
78+
}
79+
80+
public Type getType() {
81+
return type;
82+
}
83+
84+
public Map<String, Object> getMetadata() {
85+
return metadata;
86+
}
87+
88+
/**
89+
* Return a List of RoleReferences that represents role definitions associated to the subject.
90+
* The final role of this subject should be the intersection of all role references in the list.
91+
*/
92+
public List<RoleReference> getRoleReferences(@Nullable AnonymousUser anonymousUser) {
93+
switch (type) {
94+
case USER:
95+
return buildRoleReferencesForUser(anonymousUser);
96+
case API_KEY:
97+
return buildRoleReferencesForApiKey();
98+
case SERVICE_ACCOUNT:
99+
return List.of(new RoleReference.ServiceAccountRoleReference(user.principal()));
100+
default:
101+
assert false : "unknown subject type: [" + type + "]";
102+
throw new IllegalStateException("unknown subject type: [" + type + "]");
103+
}
104+
}
105+
106+
private List<RoleReference> buildRoleReferencesForUser(AnonymousUser anonymousUser) {
107+
if (user.equals(anonymousUser)) {
108+
return List.of(new RoleReference.NamedRoleReference(user.roles()));
109+
}
110+
final String[] allRoleNames;
111+
if (anonymousUser == null || false == anonymousUser.enabled()) {
112+
allRoleNames = user.roles();
113+
} else {
114+
// TODO: should we validate enable status and length of role names on instantiation time of anonymousUser?
115+
if (anonymousUser.roles().length == 0) {
116+
throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles");
117+
}
118+
allRoleNames = ArrayUtils.concat(user.roles(), anonymousUser.roles());
119+
}
120+
return List.of(new RoleReference.NamedRoleReference(allRoleNames));
121+
}
122+
123+
private List<RoleReference> buildRoleReferencesForApiKey() {
124+
if (version.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
125+
return buildRolesReferenceForApiKeyBwc();
126+
}
127+
final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY);
128+
final BytesReference roleDescriptorsBytes = (BytesReference) metadata.get(API_KEY_ROLE_DESCRIPTORS_KEY);
129+
final BytesReference limitedByRoleDescriptorsBytes = getLimitedByRoleDescriptorsBytes();
130+
if (roleDescriptorsBytes == null && limitedByRoleDescriptorsBytes == null) {
131+
throw new ElasticsearchSecurityException("no role descriptors found for API key");
132+
}
133+
final RoleReference.ApiKeyRoleReference limitedByRoleReference = new RoleReference.ApiKeyRoleReference(
134+
apiKeyId,
135+
limitedByRoleDescriptorsBytes,
136+
"apikey_limited_role"
137+
);
138+
if (isEmptyRoleDescriptorsBytes(roleDescriptorsBytes)) {
139+
return List.of(limitedByRoleReference);
140+
}
141+
return List.of(new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, "apikey_role"), limitedByRoleReference);
142+
}
143+
144+
private boolean isEmptyRoleDescriptorsBytes(BytesReference roleDescriptorsBytes) {
145+
return roleDescriptorsBytes == null || (roleDescriptorsBytes.length() == 2 && "{}".equals(roleDescriptorsBytes.utf8ToString()));
146+
}
147+
148+
private List<RoleReference> buildRolesReferenceForApiKeyBwc() {
149+
final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY);
150+
final Map<String, Object> roleDescriptorsMap = getRoleDescriptorMap(API_KEY_ROLE_DESCRIPTORS_KEY);
151+
final Map<String, Object> limitedByRoleDescriptorsMap = getRoleDescriptorMap(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY);
152+
if (roleDescriptorsMap == null && limitedByRoleDescriptorsMap == null) {
153+
throw new ElasticsearchSecurityException("no role descriptors found for API key");
154+
} else {
155+
final RoleReference.BwcApiKeyRoleReference limitedByRoleReference = new RoleReference.BwcApiKeyRoleReference(
156+
apiKeyId,
157+
limitedByRoleDescriptorsMap,
158+
"_limited_role_desc"
159+
);
160+
if (roleDescriptorsMap == null || roleDescriptorsMap.isEmpty()) {
161+
return List.of(limitedByRoleReference);
162+
} else {
163+
return List.of(
164+
new RoleReference.BwcApiKeyRoleReference(apiKeyId, roleDescriptorsMap, "_role_desc"),
165+
limitedByRoleReference
166+
);
167+
}
168+
}
169+
}
170+
171+
@SuppressWarnings("unchecked")
172+
private Map<String, Object> getRoleDescriptorMap(String key) {
173+
return (Map<String, Object>) metadata.get(key);
174+
}
175+
176+
// This following fixed role descriptor is for fleet-server BWC on and before 7.14.
177+
// It is fixed and must NOT be updated when the fleet-server service account updates.
178+
private static final BytesArray FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14 = new BytesArray(
179+
"{\"elastic/fleet-server\":{\"cluster\":[\"monitor\",\"manage_own_api_key\"],"
180+
+ "\"indices\":[{\"names\":[\"logs-*\",\"metrics-*\",\"traces-*\",\"synthetics-*\","
181+
+ "\".logs-endpoint.diagnostic.collection-*\"],"
182+
+ "\"privileges\":[\"write\",\"create_index\",\"auto_configure\"],\"allow_restricted_indices\":false},"
183+
+ "{\"names\":[\".fleet-*\"],\"privileges\":[\"read\",\"write\",\"monitor\",\"create_index\",\"auto_configure\"],"
184+
+ "\"allow_restricted_indices\":false}],\"applications\":[],\"run_as\":[],\"metadata\":{},"
185+
+ "\"transient_metadata\":{\"enabled\":true}}}"
186+
);
187+
188+
private BytesReference getLimitedByRoleDescriptorsBytes() {
189+
final BytesReference bytesReference = (BytesReference) metadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY);
190+
// Unfortunate BWC bug fix code
191+
if (bytesReference.length() == 2 && "{}".equals(bytesReference.utf8ToString())) {
192+
if (ServiceAccountSettings.REALM_NAME.equals(metadata.get(AuthenticationField.API_KEY_CREATOR_REALM_NAME))
193+
&& "elastic/fleet-server".equals(user.principal())) {
194+
return FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14;
195+
}
196+
}
197+
return bytesReference;
198+
}
199+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Set;
2323
import java.util.function.Predicate;
2424

25+
// TODO: extract a Role interface so limitedRole can be more than 2 levels
2526
/**
2627
* A {@link Role} limited by another role.<br>
2728
* The effective permissions returned on {@link #authorize(String, Set, Map, FieldPermissionsCache)} call would be limited by the
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.authz.store;
9+
10+
import java.util.Objects;
11+
import java.util.Set;
12+
13+
/**
14+
* A unique identifier that can be associated to a Role. It can be used as cache key for role caching.
15+
*/
16+
public final class RoleKey {
17+
18+
public static final String ROLES_STORE_SOURCE = "roles_stores";
19+
public static final RoleKey ROLE_KEY_EMPTY = new RoleKey(Set.of(), "__empty_role");
20+
public static final RoleKey ROLE_KEY_SUPERUSER = new RoleKey(
21+
Set.of(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()),
22+
RoleKey.ROLES_STORE_SOURCE
23+
);
24+
25+
private final Set<String> names;
26+
private final String source;
27+
28+
public RoleKey(Set<String> names, String source) {
29+
this.names = Objects.requireNonNull(names);
30+
this.source = Objects.requireNonNull(source);
31+
}
32+
33+
public Set<String> getNames() {
34+
return names;
35+
}
36+
37+
public String getSource() {
38+
return source;
39+
}
40+
41+
@Override
42+
public boolean equals(Object o) {
43+
if (this == o) return true;
44+
if (o == null || getClass() != o.getClass()) return false;
45+
RoleKey roleKey = (RoleKey) o;
46+
return names.equals(roleKey.names) && source.equals(roleKey.source);
47+
}
48+
49+
@Override
50+
public int hashCode() {
51+
return Objects.hash(names, source);
52+
}
53+
54+
@Override
55+
public String toString() {
56+
return "RoleKey{" + "names=" + names + ", source='" + source + '\'' + '}';
57+
}
58+
}

0 commit comments

Comments
 (0)