Skip to content

Commit

Permalink
Extract Role interface and allow multiple level of limiting (#81403)
Browse files Browse the repository at this point in the history
This PR extract an interface from the Role class. This helped to rework
the LimitedRole class so it no longer has the constraint of one level of
limiting.

Resolves: #81192
Relates: #80117
  • Loading branch information
ywangd committed Jan 19, 2022
1 parent c98833f commit c491279
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.authz.store.RoleReference;
import org.elasticsearch.xpack.core.security.authz.store.RoleReferenceIntersection;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.User;

import java.util.List;
import java.util.Map;

import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES;
Expand Down Expand Up @@ -85,27 +85,23 @@ public Map<String, Object> getMetadata() {
return metadata;
}

/**
* Return a List of RoleReferences that represents role definitions associated to the subject.
* The final role of this subject should be the intersection of all role references in the list.
*/
public List<RoleReference> getRoleReferences(@Nullable AnonymousUser anonymousUser) {
public RoleReferenceIntersection getRoleReferenceIntersection(@Nullable AnonymousUser anonymousUser) {
switch (type) {
case USER:
return buildRoleReferencesForUser(anonymousUser);
case API_KEY:
return buildRoleReferencesForApiKey();
case SERVICE_ACCOUNT:
return List.of(new RoleReference.ServiceAccountRoleReference(user.principal()));
return new RoleReferenceIntersection(new RoleReference.ServiceAccountRoleReference(user.principal()));
default:
assert false : "unknown subject type: [" + type + "]";
throw new IllegalStateException("unknown subject type: [" + type + "]");
}
}

private List<RoleReference> buildRoleReferencesForUser(AnonymousUser anonymousUser) {
private RoleReferenceIntersection buildRoleReferencesForUser(AnonymousUser anonymousUser) {
if (user.equals(anonymousUser)) {
return List.of(new RoleReference.NamedRoleReference(user.roles()));
return new RoleReferenceIntersection(new RoleReference.NamedRoleReference(user.roles()));
}
final String[] allRoleNames;
if (anonymousUser == null || false == anonymousUser.enabled()) {
Expand All @@ -117,10 +113,10 @@ private List<RoleReference> buildRoleReferencesForUser(AnonymousUser anonymousUs
}
allRoleNames = ArrayUtils.concat(user.roles(), anonymousUser.roles());
}
return List.of(new RoleReference.NamedRoleReference(allRoleNames));
return new RoleReferenceIntersection(new RoleReference.NamedRoleReference(allRoleNames));
}

private List<RoleReference> buildRoleReferencesForApiKey() {
private RoleReferenceIntersection buildRoleReferencesForApiKey() {
if (version.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
return buildRolesReferenceForApiKeyBwc();
}
Expand All @@ -136,9 +132,9 @@ private List<RoleReference> buildRoleReferencesForApiKey() {
RoleReference.ApiKeyRoleType.LIMITED_BY
);
if (isEmptyRoleDescriptorsBytes(roleDescriptorsBytes)) {
return List.of(limitedByRoleReference);
return new RoleReferenceIntersection(limitedByRoleReference);
}
return List.of(
return new RoleReferenceIntersection(
new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, RoleReference.ApiKeyRoleType.ASSIGNED),
limitedByRoleReference
);
Expand All @@ -148,7 +144,7 @@ private boolean isEmptyRoleDescriptorsBytes(BytesReference roleDescriptorsBytes)
return roleDescriptorsBytes == null || (roleDescriptorsBytes.length() == 2 && "{}".equals(roleDescriptorsBytes.utf8ToString()));
}

private List<RoleReference> buildRolesReferenceForApiKeyBwc() {
private RoleReferenceIntersection buildRolesReferenceForApiKeyBwc() {
final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY);
final Map<String, Object> roleDescriptorsMap = getRoleDescriptorMap(API_KEY_ROLE_DESCRIPTORS_KEY);
final Map<String, Object> limitedByRoleDescriptorsMap = getRoleDescriptorMap(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY);
Expand All @@ -161,9 +157,9 @@ private List<RoleReference> buildRolesReferenceForApiKeyBwc() {
RoleReference.ApiKeyRoleType.LIMITED_BY
);
if (roleDescriptorsMap == null || roleDescriptorsMap.isEmpty()) {
return List.of(limitedByRoleReference);
return new RoleReferenceIntersection(limitedByRoleReference);
} else {
return List.of(
return new RoleReferenceIntersection(
new RoleReference.BwcApiKeyRoleReference(apiKeyId, roleDescriptorsMap, RoleReference.ApiKeyRoleType.ASSIGNED),
limitedByRoleReference
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,30 @@
import java.util.Set;
import java.util.function.Predicate;

// TODO: extract a Role interface so limitedRole can be more than 2 levels
/**
* A {@link Role} limited by another role.<br>
* The effective permissions returned on {@link #authorize(String, Set, Map, FieldPermissionsCache)} call would be limited by the
* provided role.
*/
public final class LimitedRole extends Role {
private final Role limitedBy;

LimitedRole(
ClusterPermission cluster,
IndicesPermission indices,
ApplicationPermission application,
RunAsPermission runAs,
Role limitedBy
) {
super(Objects.requireNonNull(limitedBy, "limiting role is required").names(), cluster, indices, application, runAs);
this.limitedBy = limitedBy;
public final class LimitedRole implements Role {
private final Role baseRole;
private final Role limitedByRole;

/**
* Create a new role defined by given role and the limited role.
*
* @param baseRole existing role {@link Role}
* @param limitedByRole restrict the newly formed role to the permissions defined by this limited {@link Role}
*/
public LimitedRole(Role baseRole, Role limitedByRole) {
this.baseRole = Objects.requireNonNull(baseRole);
this.limitedByRole = Objects.requireNonNull(limitedByRole, "limited by role is required to create limited role");
}

@Override
public String[] names() {
// TODO: this is to retain existing behaviour, but it is not accurate
return limitedByRole.names();
}

@Override
Expand All @@ -64,7 +70,7 @@ public RunAsPermission runAs() {

@Override
public boolean hasFieldOrDocumentLevelSecurity() {
return super.hasFieldOrDocumentLevelSecurity() || limitedBy.hasFieldOrDocumentLevelSecurity();
return baseRole.hasFieldOrDocumentLevelSecurity() || limitedByRole.hasFieldOrDocumentLevelSecurity();
}

@Override
Expand All @@ -75,16 +81,13 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
if (super.equals(o) == false) {
return false;
}
LimitedRole that = (LimitedRole) o;
return this.limitedBy.equals(that.limitedBy);
return baseRole.equals(that.baseRole) && this.limitedByRole.equals(that.limitedByRole);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), limitedBy);
return Objects.hash(baseRole, limitedByRole);
}

@Override
Expand All @@ -94,13 +97,13 @@ public IndicesAccessControl authorize(
Map<String, IndexAbstraction> aliasAndIndexLookup,
FieldPermissionsCache fieldPermissionsCache
) {
IndicesAccessControl indicesAccessControl = super.authorize(
IndicesAccessControl indicesAccessControl = baseRole.authorize(
action,
requestedIndicesOrAliases,
aliasAndIndexLookup,
fieldPermissionsCache
);
IndicesAccessControl limitedByIndicesAccessControl = limitedBy.authorize(
IndicesAccessControl limitedByIndicesAccessControl = limitedByRole.authorize(
action,
requestedIndicesOrAliases,
aliasAndIndexLookup,
Expand All @@ -115,15 +118,15 @@ public IndicesAccessControl authorize(
*/
@Override
public Predicate<IndexAbstraction> allowedIndicesMatcher(String action) {
Predicate<IndexAbstraction> predicate = super.indices().allowedIndicesMatcher(action);
predicate = predicate.and(limitedBy.indices().allowedIndicesMatcher(action));
Predicate<IndexAbstraction> predicate = baseRole.indices().allowedIndicesMatcher(action);
predicate = predicate.and(limitedByRole.indices().allowedIndicesMatcher(action));
return predicate;
}

@Override
public Automaton allowedActionsMatcher(String index) {
final Automaton allowedMatcher = super.allowedActionsMatcher(index);
final Automaton limitedByMatcher = limitedBy.allowedActionsMatcher(index);
final Automaton allowedMatcher = baseRole.allowedActionsMatcher(index);
final Automaton limitedByMatcher = limitedByRole.allowedActionsMatcher(index);
return Automatons.intersectAndMinimize(allowedMatcher, limitedByMatcher);
}

Expand All @@ -135,7 +138,7 @@ public Automaton allowedActionsMatcher(String index) {
*/
@Override
public boolean checkIndicesAction(String action) {
return super.checkIndicesAction(action) && limitedBy.checkIndicesAction(action);
return baseRole.checkIndicesAction(action) && limitedByRole.checkIndicesAction(action);
}

/**
Expand All @@ -155,12 +158,9 @@ public ResourcePrivilegesMap checkIndicesPrivileges(
boolean allowRestrictedIndices,
Set<String> checkForPrivileges
) {
ResourcePrivilegesMap resourcePrivilegesMap = super.indices().checkResourcePrivileges(
checkForIndexPatterns,
allowRestrictedIndices,
checkForPrivileges
);
ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedBy.indices()
ResourcePrivilegesMap resourcePrivilegesMap = baseRole.indices()
.checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges);
ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedByRole.indices()
.checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges);
return ResourcePrivilegesMap.intersection(resourcePrivilegesMap, resourcePrivilegesMapForLimitedRole);
}
Expand All @@ -177,7 +177,8 @@ public ResourcePrivilegesMap checkIndicesPrivileges(
*/
@Override
public boolean checkClusterAction(String action, TransportRequest request, Authentication authentication) {
return super.checkClusterAction(action, request, authentication) && limitedBy.checkClusterAction(action, request, authentication);
return baseRole.checkClusterAction(action, request, authentication)
&& limitedByRole.checkClusterAction(action, request, authentication);
}

/**
Expand All @@ -189,7 +190,7 @@ public boolean checkClusterAction(String action, TransportRequest request, Authe
*/
@Override
public boolean grants(ClusterPrivilege clusterPrivilege) {
return super.grants(clusterPrivilege) && limitedBy.grants(clusterPrivilege);
return baseRole.grants(clusterPrivilege) && limitedByRole.grants(clusterPrivilege);
}

/**
Expand All @@ -212,31 +213,15 @@ public ResourcePrivilegesMap checkApplicationResourcePrivileges(
Set<String> checkForPrivilegeNames,
Collection<ApplicationPrivilegeDescriptor> storedPrivileges
) {
ResourcePrivilegesMap resourcePrivilegesMap = super.application().checkResourcePrivileges(
applicationName,
checkForResources,
checkForPrivilegeNames,
storedPrivileges
);
ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedBy.application()
ResourcePrivilegesMap resourcePrivilegesMap = baseRole.application()
.checkResourcePrivileges(applicationName, checkForResources, checkForPrivilegeNames, storedPrivileges);
ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedByRole.application()
.checkResourcePrivileges(applicationName, checkForResources, checkForPrivilegeNames, storedPrivileges);
return ResourcePrivilegesMap.intersection(resourcePrivilegesMap, resourcePrivilegesMapForLimitedRole);
}

@Override
public boolean checkRunAs(String runAs) {
return super.checkRunAs(runAs) && limitedBy.checkRunAs(runAs);
}

/**
* Create a new role defined by given role and the limited role.
*
* @param fromRole existing role {@link Role}
* @param limitedByRole restrict the newly formed role to the permissions defined by this limited {@link Role}
* @return {@link LimitedRole}
*/
public static LimitedRole createLimitedRole(Role fromRole, Role limitedByRole) {
Objects.requireNonNull(limitedByRole, "limited by role is required to create limited role");
return new LimitedRole(fromRole.cluster(), fromRole.indices(), fromRole.application(), fromRole.runAs(), limitedByRole);
return baseRole.checkRunAs(runAs) && limitedByRole.checkRunAs(runAs);
}
}

0 comments on commit c491279

Please sign in to comment.