security: include anonymous roles when building the global permission
The anonymous role was being applied to other users for index access control but was not being applied in terms of action level access control. This change makes the minimum required change to apply the anonymous role for all users when anonymous is enabled. Additionally, some minor changes were made to the native roles store to not lookup roles before the service is started. Closes elastic/elasticsearch#3711 Original commit: elastic/x-pack-elasticsearch@a9398e178d
This commit is contained in:
parent
4e00ab2f2b
commit
9a1d33d863
|
@ -53,6 +53,7 @@ import org.elasticsearch.xpack.security.user.XPackUser;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -308,19 +309,20 @@ public class AuthorizationService extends AbstractComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private GlobalPermission permission(String[] roleNames) {
|
private GlobalPermission permission(String[] roleNames) {
|
||||||
|
final String[] anonymousRoles = isAnonymousEnabled ? anonymousUser.roles() : Strings.EMPTY_ARRAY;
|
||||||
if (roleNames.length == 0) {
|
if (roleNames.length == 0) {
|
||||||
return DefaultRole.INSTANCE;
|
if (anonymousRoles.length == 0) {
|
||||||
}
|
assert isAnonymousEnabled == false : "anonymous is only enabled when the anonymous user has roles";
|
||||||
|
return DefaultRole.INSTANCE;
|
||||||
if (roleNames.length == 1) {
|
}
|
||||||
Role role = rolesStore.role(roleNames[0]);
|
|
||||||
return role == null ? DefaultRole.INSTANCE : GlobalPermission.Compound.builder().add(DefaultRole.INSTANCE).add(role).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we'll take all the roles and combine their associated permissions
|
// we'll take all the roles and combine their associated permissions
|
||||||
|
final Set<String> uniqueNames = new HashSet<>(Arrays.asList(roleNames));
|
||||||
|
uniqueNames.addAll(Arrays.asList(anonymousRoles));
|
||||||
|
|
||||||
GlobalPermission.Compound.Builder roles = GlobalPermission.Compound.builder().add(DefaultRole.INSTANCE);
|
GlobalPermission.Compound.Builder roles = GlobalPermission.Compound.builder().add(DefaultRole.INSTANCE);
|
||||||
for (String roleName : roleNames) {
|
for (String roleName : uniqueNames) {
|
||||||
Role role = rolesStore.role(roleName);
|
Role role = rolesStore.role(roleName);
|
||||||
if (role != null) {
|
if (role != null) {
|
||||||
roles.add(role);
|
roles.add(role);
|
||||||
|
|
|
@ -354,6 +354,9 @@ public class NativeRolesStore extends AbstractComponent implements RolesStore, C
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Role role(String roleName) {
|
public Role role(String roleName) {
|
||||||
|
if (state() != State.STARTED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
RoleAndVersion roleAndVersion = getRoleAndVersion(roleName);
|
RoleAndVersion roleAndVersion = getRoleAndVersion(roleName);
|
||||||
return roleAndVersion == null ? null : roleAndVersion.getRole();
|
return roleAndVersion == null ? null : roleAndVersion.getRole();
|
||||||
}
|
}
|
||||||
|
@ -443,6 +446,10 @@ public class NativeRolesStore extends AbstractComponent implements RolesStore, C
|
||||||
}
|
}
|
||||||
|
|
||||||
private RoleAndVersion getRoleAndVersion(final String roleId) {
|
private RoleAndVersion getRoleAndVersion(final String roleId) {
|
||||||
|
if (securityIndexExists == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
RoleAndVersion roleAndVersion = null;
|
RoleAndVersion roleAndVersion = null;
|
||||||
final AtomicReference<GetResponse> getRef = new AtomicReference<>(null);
|
final AtomicReference<GetResponse> getRef = new AtomicReference<>(null);
|
||||||
final CountDownLatch latch = new CountDownLatch(1);
|
final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
|
@ -49,6 +49,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
|
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTimeout;
|
||||||
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
|
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
|
||||||
import static org.hamcrest.Matchers.arrayContaining;
|
import static org.hamcrest.Matchers.arrayContaining;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
@ -316,10 +317,12 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase {
|
||||||
.addIndices(new String[]{"*"}, new String[]{"read"},
|
.addIndices(new String[]{"*"}, new String[]{"read"},
|
||||||
new FieldPermissions(new String[]{"body", "title"}, null), new BytesArray("{\"match_all\": {}}"))
|
new FieldPermissions(new String[]{"body", "title"}, null), new BytesArray("{\"match_all\": {}}"))
|
||||||
.get();
|
.get();
|
||||||
try {
|
if (anonymousEnabled) {
|
||||||
client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get();
|
assertNoTimeout(client()
|
||||||
fail("user should not be able to execute any cluster actions!");
|
.filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get());
|
||||||
} catch (ElasticsearchSecurityException e) {
|
} else {
|
||||||
|
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> client()
|
||||||
|
.filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get());
|
||||||
assertThat(e.status(), is(RestStatus.FORBIDDEN));
|
assertThat(e.status(), is(RestStatus.FORBIDDEN));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -358,10 +361,12 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase {
|
||||||
.prepareHealth().get();
|
.prepareHealth().get();
|
||||||
assertFalse(response.isTimedOut());
|
assertFalse(response.isTimedOut());
|
||||||
c.prepareDeleteRole("test_role").get();
|
c.prepareDeleteRole("test_role").get();
|
||||||
try {
|
if (anonymousEnabled) {
|
||||||
client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get();
|
assertNoTimeout(
|
||||||
fail("user should not be able to execute any actions!");
|
client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get());
|
||||||
} catch (ElasticsearchSecurityException e) {
|
} else {
|
||||||
|
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () ->
|
||||||
|
client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get());
|
||||||
assertThat(e.status(), is(RestStatus.FORBIDDEN));
|
assertThat(e.status(), is(RestStatus.FORBIDDEN));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -406,11 +411,14 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase {
|
||||||
assertThat(joe.roles(), arrayContaining("read_role"));
|
assertThat(joe.roles(), arrayContaining("read_role"));
|
||||||
assertThat(joe.fullName(), is("Joe Smith"));
|
assertThat(joe.fullName(), is("Joe Smith"));
|
||||||
|
|
||||||
// test that role change took effect
|
// test that role change took effect if anonymous is disabled as anonymous grants monitoring permissions...
|
||||||
try {
|
if (anonymousEnabled) {
|
||||||
client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get();
|
assertNoTimeout(
|
||||||
fail("test_role does not have permission to get health");
|
client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get());
|
||||||
} catch (ElasticsearchSecurityException e) {
|
} else {
|
||||||
|
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () ->
|
||||||
|
client().filterWithHeader(Collections.singletonMap("Authorization", token)).admin().cluster().prepareHealth().get());
|
||||||
|
assertThat(e.status(), is(RestStatus.FORBIDDEN));
|
||||||
assertThat(e.getMessage(), containsString("authorized"));
|
assertThat(e.getMessage(), containsString("authorized"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,7 +442,8 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase {
|
||||||
// test with new password and role
|
// test with new password and role
|
||||||
response = client()
|
response = client()
|
||||||
.filterWithHeader(
|
.filterWithHeader(
|
||||||
Collections.singletonMap("Authorization",basicAuthHeaderValue("joe", new SecuredString("changeme2".toCharArray()))))
|
Collections.singletonMap("Authorization",
|
||||||
|
basicAuthHeaderValue("joe", new SecuredString("changeme2".toCharArray()))))
|
||||||
.admin().cluster().prepareHealth().get();
|
.admin().cluster().prepareHealth().get();
|
||||||
assertFalse(response.isTimedOut());
|
assertFalse(response.isTimedOut());
|
||||||
}
|
}
|
||||||
|
@ -558,7 +567,8 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase {
|
||||||
|
|
||||||
response = client()
|
response = client()
|
||||||
.filterWithHeader(
|
.filterWithHeader(
|
||||||
Collections.singletonMap("Authorization", basicAuthHeaderValue("joe", new SecuredString("changeme".toCharArray()))))
|
Collections.singletonMap("Authorization",
|
||||||
|
basicAuthHeaderValue("joe", new SecuredString("changeme".toCharArray()))))
|
||||||
.admin().cluster().prepareHealth().get();
|
.admin().cluster().prepareHealth().get();
|
||||||
assertThat(response.isTimedOut(), is(false));
|
assertThat(response.isTimedOut(), is(false));
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
|
||||||
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
|
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
|
||||||
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
|
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
|
||||||
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
|
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
|
||||||
|
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsAction;
|
||||||
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest;
|
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest;
|
||||||
import org.elasticsearch.action.admin.indices.recovery.RecoveryAction;
|
import org.elasticsearch.action.admin.indices.recovery.RecoveryAction;
|
||||||
import org.elasticsearch.action.admin.indices.recovery.RecoveryRequest;
|
import org.elasticsearch.action.admin.indices.recovery.RecoveryRequest;
|
||||||
|
@ -614,6 +615,32 @@ public class AuthorizationServiceTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testAnonymousRolesAreAppliedToOtherUsers() {
|
||||||
|
TransportRequest request = new ClusterHealthRequest();
|
||||||
|
ClusterState state = mock(ClusterState.class);
|
||||||
|
Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous_user_role").build();
|
||||||
|
final AnonymousUser anonymousUser = new AnonymousUser(settings);
|
||||||
|
authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail,
|
||||||
|
new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser);
|
||||||
|
|
||||||
|
when(rolesStore.role("anonymous_user_role"))
|
||||||
|
.thenReturn(Role.builder("anonymous_user_role")
|
||||||
|
.cluster(ClusterPrivilege.ALL)
|
||||||
|
.add(IndexPrivilege.ALL, "a")
|
||||||
|
.build());
|
||||||
|
when(clusterService.state()).thenReturn(state);
|
||||||
|
when(state.metaData()).thenReturn(MetaData.EMPTY_META_DATA);
|
||||||
|
|
||||||
|
// sanity check the anonymous user
|
||||||
|
authorizationService.authorize(createAuthentication(anonymousUser), ClusterHealthAction.NAME, request);
|
||||||
|
authorizationService.authorize(createAuthentication(anonymousUser), IndicesExistsAction.NAME, new IndicesExistsRequest("a"));
|
||||||
|
|
||||||
|
// test the no role user
|
||||||
|
final User userWithNoRoles = new User("no role user");
|
||||||
|
authorizationService.authorize(createAuthentication(userWithNoRoles), ClusterHealthAction.NAME, request);
|
||||||
|
authorizationService.authorize(createAuthentication(userWithNoRoles), IndicesExistsAction.NAME, new IndicesExistsRequest("a"));
|
||||||
|
}
|
||||||
|
|
||||||
private Authentication createAuthentication(User user) {
|
private Authentication createAuthentication(User user) {
|
||||||
RealmRef lookedUpBy = user.runAs() == null ? null : new RealmRef("looked", "up", "by");
|
RealmRef lookedUpBy = user.runAs() == null ? null : new RealmRef("looked", "up", "by");
|
||||||
return new Authentication(user, new RealmRef("test", "test", "foo"), lookedUpBy);
|
return new Authentication(user, new RealmRef("test", "test", "foo"), lookedUpBy);
|
||||||
|
|
Loading…
Reference in New Issue