This introduces a role-mapping API to X-Pack security.

Features:
- A `GET`/`PUT`/`DELETE` API at `/_xpack/security/role_mapping/`
- Role-mappings are stored in the `.security` index 
- A custom expression language (in JSON) for expressing the mapping rules 
- Supported in LDAP/AD and PKI realms
- LDAP realm also supports loading arbitrary meta-data (which can be used in the mapping rules)
- A CompositeRoleMapper unifies roles from the existing file based mapper, and the new API based mapper.
- Usage stats for native role mappings

Original commit: elastic/x-pack-elasticsearch@d9972ed1da
This commit is contained in:
Tim Vernum 2017-05-04 13:38:50 +10:00 committed by GitHub
parent cf27cb479a
commit 6adf4fd3af
70 changed files with 4249 additions and 276 deletions

View File

@ -458,6 +458,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I
entries.addAll(watcher.getNamedWriteables());
entries.addAll(machineLearning.getNamedWriteables());
entries.addAll(licensing.getNamedWriteables());
entries.addAll(Security.getNamedWriteables());
return entries;
}
@ -489,6 +490,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I
public static boolean isTribeNode(Settings settings) {
return settings.getGroups("tribe", true).isEmpty() == false;
}
public static boolean isTribeClientNode(Settings settings) {
return settings.get("tribe.name") != null;
}

View File

@ -73,6 +73,12 @@ import org.elasticsearch.xpack.security.action.role.TransportClearRolesCacheActi
import org.elasticsearch.xpack.security.action.role.TransportDeleteRoleAction;
import org.elasticsearch.xpack.security.action.role.TransportGetRolesAction;
import org.elasticsearch.xpack.security.action.role.TransportPutRoleAction;
import org.elasticsearch.xpack.security.action.rolemapping.DeleteRoleMappingAction;
import org.elasticsearch.xpack.security.action.rolemapping.GetRoleMappingsAction;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingAction;
import org.elasticsearch.xpack.security.action.rolemapping.TransportDeleteRoleMappingAction;
import org.elasticsearch.xpack.security.action.rolemapping.TransportGetRoleMappingsAction;
import org.elasticsearch.xpack.security.action.rolemapping.TransportPutRoleMappingAction;
import org.elasticsearch.xpack.security.action.token.CreateTokenAction;
import org.elasticsearch.xpack.security.action.token.InvalidateTokenAction;
import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction;
@ -109,6 +115,8 @@ import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.ExpressionParser;
import org.elasticsearch.xpack.security.authz.AuthorizationService;
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.security.authz.accesscontrol.OptOutQueryCache;
@ -130,6 +138,9 @@ import org.elasticsearch.xpack.security.rest.action.role.RestClearRolesCacheActi
import org.elasticsearch.xpack.security.rest.action.role.RestDeleteRoleAction;
import org.elasticsearch.xpack.security.rest.action.role.RestGetRolesAction;
import org.elasticsearch.xpack.security.rest.action.role.RestPutRoleAction;
import org.elasticsearch.xpack.security.rest.action.rolemapping.RestDeleteRoleMappingAction;
import org.elasticsearch.xpack.security.rest.action.rolemapping.RestGetRoleMappingsAction;
import org.elasticsearch.xpack.security.rest.action.rolemapping.RestPutRoleMappingAction;
import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction;
import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction;
import org.elasticsearch.xpack.security.rest.action.user.RestGetUsersAction;
@ -243,6 +254,7 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
b.bind(CryptoService.class).toProvider(Providers.of(null));
b.bind(Realms.class).toProvider(Providers.of(null)); // for SecurityFeatureSet
b.bind(CompositeRolesStore.class).toProvider(Providers.of(null)); // for SecurityFeatureSet
b.bind(NativeRoleMappingStore.class).toProvider(Providers.of(null)); // for SecurityFeatureSet
b.bind(AuditTrailService.class)
.toInstance(new AuditTrailService(settings, Collections.emptyList(), licenseState));
});
@ -298,8 +310,7 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
}
}
final AuditTrailService auditTrailService =
new AuditTrailService(settings,
auditTrails.stream().collect(Collectors.toList()), licenseState);
new AuditTrailService(settings, auditTrails.stream().collect(Collectors.toList()), licenseState);
components.add(auditTrailService);
final SecurityLifecycleService securityLifecycleService =
@ -308,24 +319,25 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
components.add(tokenService);
// realms construction
final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client,
securityLifecycleService);
final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, securityLifecycleService);
final NativeRoleMappingStore nativeRoleMappingStore = new NativeRoleMappingStore(settings, client, securityLifecycleService);
final AnonymousUser anonymousUser = new AnonymousUser(settings);
final ReservedRealm reservedRealm = new ReservedRealm(env, settings, nativeUsersStore,
anonymousUser, securityLifecycleService, threadPool.getThreadContext());
Map<String, Realm.Factory> realmFactories = new HashMap<>();
realmFactories.putAll(InternalRealms.getFactories(threadPool, resourceWatcherService, sslService, nativeUsersStore));
realmFactories.putAll(InternalRealms.getFactories(threadPool, resourceWatcherService,
sslService, nativeUsersStore, nativeRoleMappingStore));
for (XPackExtension extension : extensions) {
Map<String, Realm.Factory> newRealms = extension.getRealms(resourceWatcherService);
for (Map.Entry<String, Realm.Factory> entry : newRealms.entrySet()) {
if (realmFactories.put(entry.getKey(), entry.getValue()) != null) {
throw new IllegalArgumentException("Realm type [" + entry.getKey() +
"] is already registered");
throw new IllegalArgumentException("Realm type [" + entry.getKey() + "] is already registered");
}
}
}
final Realms realms = new Realms(settings, env, realmFactories, licenseState, threadPool.getThreadContext(), reservedRealm);
components.add(nativeUsersStore);
components.add(nativeRoleMappingStore);
components.add(realms);
components.add(reservedRealm);
@ -334,7 +346,7 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
for (XPackExtension extension : extensions) {
AuthenticationFailureHandler extensionFailureHandler = extension.getAuthenticationFailureHandler();
if (extensionFailureHandler != null && failureHandler != null) {
throw new IllegalStateException("Extensions [" + extensionName +"] and [" + extension.name() + "] " +
throw new IllegalStateException("Extensions [" + extensionName + "] and [" + extension.name() + "] " +
"both set an authentication failure handler");
}
failureHandler = extensionFailureHandler;
@ -532,7 +544,8 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
if (enabled == false) {
return emptyList();
}
return Arrays.asList(new ActionHandler<>(ClearRealmCacheAction.INSTANCE, TransportClearRealmCacheAction.class),
return Arrays.asList(
new ActionHandler<>(ClearRealmCacheAction.INSTANCE, TransportClearRealmCacheAction.class),
new ActionHandler<>(ClearRolesCacheAction.INSTANCE, TransportClearRolesCacheAction.class),
new ActionHandler<>(GetUsersAction.INSTANCE, TransportGetUsersAction.class),
new ActionHandler<>(PutUserAction.INSTANCE, TransportPutUserAction.class),
@ -544,8 +557,12 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
new ActionHandler<>(AuthenticateAction.INSTANCE, TransportAuthenticateAction.class),
new ActionHandler<>(SetEnabledAction.INSTANCE, TransportSetEnabledAction.class),
new ActionHandler<>(HasPrivilegesAction.INSTANCE, TransportHasPrivilegesAction.class),
new ActionHandler<>(GetRoleMappingsAction.INSTANCE, TransportGetRoleMappingsAction.class),
new ActionHandler<>(PutRoleMappingAction.INSTANCE, TransportPutRoleMappingAction.class),
new ActionHandler<>(DeleteRoleMappingAction.INSTANCE, TransportDeleteRoleMappingAction.class),
new ActionHandler<>(CreateTokenAction.INSTANCE, TransportCreateTokenAction.class),
new ActionHandler<>(InvalidateTokenAction.INSTANCE, TransportInvalidateTokenAction.class));
new ActionHandler<>(InvalidateTokenAction.INSTANCE, TransportInvalidateTokenAction.class)
);
}
@Override
@ -580,8 +597,12 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
new RestChangePasswordAction(settings, restController, securityContext.get()),
new RestSetEnabledAction(settings, restController),
new RestHasPrivilegesAction(settings, restController, securityContext.get()),
new RestGetRoleMappingsAction(settings, restController),
new RestPutRoleMappingAction(settings, restController),
new RestDeleteRoleMappingAction(settings, restController),
new RestGetTokenAction(settings, licenseState, restController),
new RestInvalidateTokenAction(settings, licenseState, restController));
new RestInvalidateTokenAction(settings, licenseState, restController)
);
}
@Override
@ -816,6 +837,10 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
return handler -> new SecurityRestFilter(settings, licenseState, sslService, threadContext, authcService.get(), handler);
}
public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
return Arrays.asList(ExpressionParser.NAMED_WRITEABLES);
}
public List<ExecutorBuilder<?>> getExecutorBuilders(final Settings settings) {
if (enabled && transportClientMode == false) {
return Collections.singletonList(

View File

@ -10,6 +10,7 @@ import java.nio.file.Files;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.Nullable;
@ -17,6 +18,7 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.CountDown;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.env.Environment;
import org.elasticsearch.license.XPackLicenseState;
@ -24,11 +26,13 @@ import org.elasticsearch.xpack.XPackFeatureSet;
import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.security.authc.Realms;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.transport.filter.IPFilter;
import org.elasticsearch.xpack.security.user.AnonymousUser;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.xpack.XPackSettings.HTTP_SSL_ENABLED;
/**
@ -44,17 +48,21 @@ public class SecurityFeatureSet implements XPackFeatureSet {
@Nullable
private final CompositeRolesStore rolesStore;
@Nullable
private final NativeRoleMappingStore roleMappingStore;
@Nullable
private final IPFilter ipFilter;
private final boolean systemKeyUsed;
@Inject
public SecurityFeatureSet(Settings settings, @Nullable XPackLicenseState licenseState, @Nullable Realms realms,
@Nullable CompositeRolesStore rolesStore, @Nullable IPFilter ipFilter,
Environment environment) {
public SecurityFeatureSet(Settings settings, @Nullable XPackLicenseState licenseState,
@Nullable Realms realms, @Nullable CompositeRolesStore rolesStore,
@Nullable NativeRoleMappingStore roleMappingStore,
@Nullable IPFilter ipFilter, Environment environment) {
this.enabled = XPackSettings.SECURITY_ENABLED.get(settings);
this.licenseState = licenseState;
this.realms = realms;
this.rolesStore = rolesStore;
this.roleMappingStore = roleMappingStore;
this.settings = settings;
this.ipFilter = ipFilter;
this.systemKeyUsed = enabled && Files.exists(CryptoService.resolveSystemKey(environment));
@ -92,15 +100,42 @@ public class SecurityFeatureSet implements XPackFeatureSet {
Map<String, Object> auditUsage = auditUsage(settings);
Map<String, Object> ipFilterUsage = ipFilterUsage(ipFilter);
Map<String, Object> systemKeyUsage = systemKeyUsage();
Map<String, Object> anonymousUsage = Collections.singletonMap("enabled", AnonymousUser.isAnonymousEnabled(settings));
Map<String, Object> anonymousUsage = singletonMap("enabled", AnonymousUser.isAnonymousEnabled(settings));
final AtomicReference<Map<String, Object>> rolesUsageRef = new AtomicReference<>();
final AtomicReference<Map<String, Object>> roleMappingUsageRef = new AtomicReference<>();
final CountDown countDown = new CountDown(2);
final Runnable doCountDown = () -> {
if (countDown.countDown()) {
listener.onResponse(new Usage(available(), enabled(), realmsUsage,
rolesUsageRef.get(), roleMappingUsageRef.get(),
sslUsage, auditUsage, ipFilterUsage, systemKeyUsage, anonymousUsage));
}
};
final ActionListener<Map<String, Object>> rolesStoreUsageListener =
ActionListener.wrap(rolesStoreUsage -> listener.onResponse(new Usage(available(), enabled(), realmsUsage, rolesStoreUsage,
sslUsage, auditUsage, ipFilterUsage, systemKeyUsage, anonymousUsage)),listener::onFailure);
ActionListener.wrap(rolesStoreUsage -> {
rolesUsageRef.set(rolesStoreUsage);
doCountDown.run();
}, listener::onFailure);
final ActionListener<Map<String, Object>> roleMappingStoreUsageListener =
ActionListener.wrap(nativeRoleMappingStoreUsage -> {
Map<String, Object> usage = singletonMap("native", nativeRoleMappingStoreUsage);
roleMappingUsageRef.set(usage);
doCountDown.run();
}, listener::onFailure);
if (rolesStore == null) {
rolesStoreUsageListener.onResponse(Collections.emptyMap());
} else {
rolesStore.usageStats(rolesStoreUsageListener);
}
if (roleMappingStore == null) {
roleMappingStoreUsageListener.onResponse(Collections.emptyMap());
} else {
roleMappingStore.usageStats(roleMappingStoreUsageListener);
}
}
static Map<String, Object> buildRealmsUsage(Realms realms) {
@ -111,7 +146,7 @@ public class SecurityFeatureSet implements XPackFeatureSet {
}
static Map<String, Object> sslUsage(Settings settings) {
return Collections.singletonMap("http", Collections.singletonMap("enabled", HTTP_SSL_ENABLED.get(settings)));
return singletonMap("http", singletonMap("enabled", HTTP_SSL_ENABLED.get(settings)));
}
static Map<String, Object> auditUsage(Settings settings) {
@ -130,13 +165,14 @@ public class SecurityFeatureSet implements XPackFeatureSet {
Map<String, Object> systemKeyUsage() {
// we can piggy back on the encryption enabled method as it is only enabled if there is a system key
return Collections.singletonMap("enabled", systemKeyUsed);
return singletonMap("enabled", systemKeyUsed);
}
public static class Usage extends XPackFeatureSet.Usage {
private static final String REALMS_XFIELD = "realms";
private static final String ROLES_XFIELD = "roles";
private static final String ROLE_MAPPING_XFIELD = "role_mapping";
private static final String SSL_XFIELD = "ssl";
private static final String AUDIT_XFIELD = "audit";
private static final String IP_FILTER_XFIELD = "ipfilter";
@ -150,6 +186,7 @@ public class SecurityFeatureSet implements XPackFeatureSet {
private Map<String, Object> ipFilterUsage;
private Map<String, Object> systemKeyUsage;
private Map<String, Object> anonymousUsage;
private Map<String, Object> roleMappingStoreUsage;
public Usage(StreamInput in) throws IOException {
super(in);
@ -160,14 +197,18 @@ public class SecurityFeatureSet implements XPackFeatureSet {
ipFilterUsage = in.readMap();
systemKeyUsage = in.readMap();
anonymousUsage = in.readMap();
roleMappingStoreUsage = in.readMap();
}
public Usage(boolean available, boolean enabled, Map<String, Object> realmsUsage, Map<String, Object> rolesStoreUsage,
Map<String, Object> sslUsage, Map<String, Object> auditUsage, Map<String, Object> ipFilterUsage,
Map<String, Object> systemKeyUsage, Map<String, Object> anonymousUsage) {
public Usage(boolean available, boolean enabled, Map<String, Object> realmsUsage,
Map<String, Object> rolesStoreUsage, Map<String, Object> roleMappingStoreUsage,
Map<String, Object> sslUsage, Map<String, Object> auditUsage,
Map<String, Object> ipFilterUsage, Map<String, Object> systemKeyUsage,
Map<String, Object> anonymousUsage) {
super(XPackPlugin.SECURITY, available, enabled);
this.realmsUsage = realmsUsage;
this.rolesStoreUsage = rolesStoreUsage;
this.roleMappingStoreUsage = roleMappingStoreUsage;
this.sslUsage = sslUsage;
this.auditUsage = auditUsage;
this.ipFilterUsage = ipFilterUsage;
@ -185,6 +226,7 @@ public class SecurityFeatureSet implements XPackFeatureSet {
out.writeMap(ipFilterUsage);
out.writeMap(systemKeyUsage);
out.writeMap(anonymousUsage);
out.writeMap(roleMappingStoreUsage);
}
@Override
@ -193,6 +235,7 @@ public class SecurityFeatureSet implements XPackFeatureSet {
if (enabled) {
builder.field(REALMS_XFIELD, realmsUsage);
builder.field(ROLES_XFIELD, rolesStoreUsage);
builder.field(ROLE_MAPPING_XFIELD, roleMappingStoreUsage);
builder.field(SSL_XFIELD, sslUsage);
builder.field(AUDIT_XFIELD, auditUsage);
builder.field(IP_FILTER_XFIELD, ipFilterUsage);

View File

@ -20,7 +20,6 @@ import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail;
import org.elasticsearch.xpack.security.authc.TokenService;
import org.elasticsearch.xpack.security.authc.esnative.NativeRealmMigrator;
import org.elasticsearch.xpack.security.support.IndexLifecycleManager;
@ -93,6 +92,7 @@ public class SecurityLifecycleService extends AbstractComponent implements Clust
}
securityIndex.clusterChanged(event);
try {
if (Security.indexAuditLoggingEnabled(settings) &&
indexAuditTrail.state() == IndexAuditTrail.State.INITIALIZED) {

View File

@ -0,0 +1,37 @@
/*
* 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.rolemapping;
import org.elasticsearch.action.Action;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.xpack.security.action.role.DeleteRoleRequest;
import org.elasticsearch.xpack.security.action.role.DeleteRoleRequestBuilder;
import org.elasticsearch.xpack.security.action.role.DeleteRoleResponse;
/**
* Action for deleting a role-mapping from the
* {@link org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore}
*/
public class DeleteRoleMappingAction extends Action<DeleteRoleMappingRequest,
DeleteRoleMappingResponse, DeleteRoleMappingRequestBuilder> {
public static final DeleteRoleMappingAction INSTANCE = new DeleteRoleMappingAction();
public static final String NAME = "cluster:admin/xpack/security/role_mapping/delete";
private DeleteRoleMappingAction() {
super(NAME);
}
@Override
public DeleteRoleMappingRequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new DeleteRoleMappingRequestBuilder(client, this);
}
@Override
public DeleteRoleMappingResponse newResponse() {
return new DeleteRoleMappingResponse();
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.rolemapping;
import java.io.IOException;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* A request delete a role-mapping from the {@link org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore}
*/
public class DeleteRoleMappingRequest extends ActionRequest implements WriteRequest<DeleteRoleMappingRequest> {
private String name;
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
public DeleteRoleMappingRequest() {
}
@Override
public DeleteRoleMappingRequest setRefreshPolicy(RefreshPolicy refreshPolicy) {
this.refreshPolicy = refreshPolicy;
return this;
}
@Override
public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}
@Override
public ActionRequestValidationException validate() {
if (name == null) {
return addValidationError("role-mapping name is missing", null);
} else {
return null;
}
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
name = in.readString();
refreshPolicy = RefreshPolicy.readFrom(in);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(name);
refreshPolicy.writeTo(out);
}
}

View File

@ -0,0 +1,29 @@
/*
* 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.rolemapping;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.support.WriteRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
/**
* A builder for requests to delete a role-mapping from the
* {@link org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore}
*/
public class DeleteRoleMappingRequestBuilder extends ActionRequestBuilder<DeleteRoleMappingRequest,
DeleteRoleMappingResponse, DeleteRoleMappingRequestBuilder>
implements WriteRequestBuilder<DeleteRoleMappingRequestBuilder> {
public DeleteRoleMappingRequestBuilder(ElasticsearchClient client,
DeleteRoleMappingAction action) {
super(client, action, new DeleteRoleMappingRequest());
}
public DeleteRoleMappingRequestBuilder name(String name) {
request.setName(name);
return this;
}
}

View File

@ -0,0 +1,59 @@
/*
* 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.rolemapping;
import java.io.IOException;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
/**
* Response for a role-mapping being deleted from the
* {@link org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore}
*/
public class DeleteRoleMappingResponse extends ActionResponse implements ToXContentObject {
private boolean found = false;
/**
* Package private for {@link DeleteRoleMappingAction#newResponse()}
*/
DeleteRoleMappingResponse() {}
DeleteRoleMappingResponse(boolean found) {
this.found = found;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject().field("found", found).endObject();
return builder;
}
/**
* If <code>true</code>, indicates the {@link DeleteRoleMappingRequest#getName() named role-mapping} was found and deleted.
* Otherwise, the role-mapping could not be found.
*/
public boolean isFound() {
return this.found;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
found = in.readBoolean();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeBoolean(found);
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.rolemapping;
import org.elasticsearch.action.Action;
import org.elasticsearch.client.ElasticsearchClient;
/**
* Action to retrieve one or more role-mappings from X-Pack security
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class GetRoleMappingsAction extends Action<GetRoleMappingsRequest, GetRoleMappingsResponse, GetRoleMappingsRequestBuilder> {
public static final GetRoleMappingsAction INSTANCE = new GetRoleMappingsAction();
public static final String NAME = "cluster:admin/xpack/security/role_mapping/get";
private GetRoleMappingsAction() {
super(NAME);
}
@Override
public GetRoleMappingsRequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new GetRoleMappingsRequestBuilder(client, this);
}
@Override
public GetRoleMappingsResponse newResponse() {
return new GetRoleMappingsResponse();
}
}

View File

@ -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.security.action.rolemapping;
import java.io.IOException;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* Request to retrieve role-mappings from X-Pack security
*
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class GetRoleMappingsRequest extends ActionRequest {
private String[] names = Strings.EMPTY_ARRAY;
public GetRoleMappingsRequest() {
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (names == null) {
validationException = addValidationError("role-mapping names are missing",
validationException);
}
return validationException;
}
/**
* Specify (by name) which role-mappings to delete.
* @see org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping#getName()
*/
public void setNames(String... names) {
this.names = names;
}
/**
* @see #setNames(String...)
*/
public String[] getNames() {
return names;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
names = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeStringArray(names);
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.rolemapping;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
/**
* Builder for a request to retrieve role-mappings from X-Pack security
*
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class GetRoleMappingsRequestBuilder extends ActionRequestBuilder<GetRoleMappingsRequest,
GetRoleMappingsResponse, GetRoleMappingsRequestBuilder> {
public GetRoleMappingsRequestBuilder(ElasticsearchClient client, GetRoleMappingsAction action) {
super(client, action, new GetRoleMappingsRequest());
}
public GetRoleMappingsRequestBuilder names(String... names) {
request.setNames(names);
return this;
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.rolemapping;
import java.io.IOException;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping;
/**
* Response to {@link GetRoleMappingsAction get role-mappings API}.
*
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class GetRoleMappingsResponse extends ActionResponse {
private ExpressionRoleMapping[] mappings;
public GetRoleMappingsResponse(ExpressionRoleMapping... mappings) {
this.mappings = mappings;
}
public ExpressionRoleMapping[] mappings() {
return mappings;
}
public boolean hasMappings() {
return mappings.length > 0;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
int size = in.readVInt();
mappings = new ExpressionRoleMapping[size];
for (int i = 0; i < size; i++) {
mappings[i] = new ExpressionRoleMapping(in);
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeVInt(mappings.length);
for (ExpressionRoleMapping mapping : mappings) {
mapping.writeTo(out);
}
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.rolemapping;
import org.elasticsearch.action.Action;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.xpack.security.action.role.PutRoleRequest;
import org.elasticsearch.xpack.security.action.role.PutRoleRequestBuilder;
import org.elasticsearch.xpack.security.action.role.PutRoleResponse;
/**
* Action for adding a role to the security index
*/
public class PutRoleMappingAction extends Action<PutRoleMappingRequest, PutRoleMappingResponse,
PutRoleMappingRequestBuilder> {
public static final PutRoleMappingAction INSTANCE = new PutRoleMappingAction();
public static final String NAME = "cluster:admin/xpack/security/role_mapping/put";
private PutRoleMappingAction() {
super(NAME);
}
@Override
public PutRoleMappingRequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new PutRoleMappingRequestBuilder(client, this);
}
@Override
public PutRoleMappingResponse newResponse() {
return new PutRoleMappingResponse();
}
}

View File

@ -0,0 +1,154 @@
/*
* 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.rolemapping;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.ExpressionParser;
import org.elasticsearch.xpack.security.support.MetadataUtils;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* Request object for adding/updating a role-mapping to the native store
*
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class PutRoleMappingRequest extends ActionRequest
implements WriteRequest<PutRoleMappingRequest> {
private String name = null;
private boolean enabled = true;
private List<String> roles = Collections.emptyList();
private RoleMapperExpression rules = null;
private Map<String, Object> metadata = Collections.emptyMap();
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
public PutRoleMappingRequest() {
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (name == null) {
validationException = addValidationError("role-mapping name is missing",
validationException);
}
if (roles.isEmpty()) {
validationException = addValidationError("role-mapping roles are missing",
validationException);
}
if (rules == null) {
validationException = addValidationError("role-mapping rules are missing",
validationException);
}
if (MetadataUtils.containsReservedMetadata(metadata)) {
validationException = addValidationError("metadata keys may not start with [" +
MetadataUtils.RESERVED_PREFIX + "]", validationException);
}
return validationException;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getRoles() {
return Collections.unmodifiableList(roles);
}
public void setRoles(List<String> roles) {
this.roles = new ArrayList<>(roles);
}
public RoleMapperExpression getRules() {
return rules;
}
public void setRules(RoleMapperExpression expression) {
this.rules = expression;
}
@Override
public PutRoleMappingRequest setRefreshPolicy(RefreshPolicy refreshPolicy) {
this.refreshPolicy = refreshPolicy;
return this;
}
/**
* Should this request trigger a refresh ({@linkplain RefreshPolicy#IMMEDIATE}, the default),
* wait for a refresh ({@linkplain RefreshPolicy#WAIT_UNTIL}), or proceed ignore refreshes
* entirely ({@linkplain RefreshPolicy#NONE}).
*/
@Override
public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}
public void setMetadata(Map<String, Object> metadata) {
this.metadata = Objects.requireNonNull(metadata);
}
public Map<String, Object> getMetadata() {
return metadata;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
this.name = in.readString();
this.enabled = in.readBoolean();
this.roles = in.readList(StreamInput::readString);
this.rules = ExpressionParser.readExpression(in);
this.metadata = in.readMap();
this.refreshPolicy = RefreshPolicy.readFrom(in);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(name);
out.writeBoolean(enabled);
out.writeStringList(roles);
ExpressionParser.writeExpression(rules, out);
out.writeMap(metadata);
refreshPolicy.writeTo(out);
}
public ExpressionRoleMapping getMapping() {
return new ExpressionRoleMapping(
name,
rules,
roles,
metadata,
enabled
);
}
}

View File

@ -0,0 +1,71 @@
/*
* 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.rolemapping;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.support.WriteRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
/**
* Builder for requests to add/update a role-mapping to the native store
*
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class PutRoleMappingRequestBuilder extends ActionRequestBuilder<PutRoleMappingRequest,
PutRoleMappingResponse, PutRoleMappingRequestBuilder> implements
WriteRequestBuilder<PutRoleMappingRequestBuilder> {
public PutRoleMappingRequestBuilder(ElasticsearchClient client, PutRoleMappingAction action) {
super(client, action, new PutRoleMappingRequest());
}
/**
* Populate the put role request from the source and the role's name
*/
public PutRoleMappingRequestBuilder source(String name, BytesReference source,
XContentType xContentType) throws IOException {
ExpressionRoleMapping mapping = ExpressionRoleMapping.parse(name, source, xContentType);
request.setName(name);
request.setEnabled(mapping.isEnabled());
request.setRoles(mapping.getRoles());
request.setRules(mapping.getExpression());
request.setMetadata(mapping.getMetadata());
return this;
}
public PutRoleMappingRequestBuilder name(String name) {
request.setName(name);
return this;
}
public PutRoleMappingRequestBuilder roles(String... roles) {
request.setRoles(Arrays.asList(roles));
return this;
}
public PutRoleMappingRequestBuilder expression(RoleMapperExpression expression) {
request.setRules(expression);
return this;
}
public PutRoleMappingRequestBuilder enabled(boolean enabled) {
request.setEnabled(enabled);
return this;
}
public PutRoleMappingRequestBuilder metadata(Map<String, Object> metadata) {
request.setMetadata(metadata);
return this;
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.rolemapping;
import java.io.IOException;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
/**
* Response when adding/updating a role-mapping.
*
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class PutRoleMappingResponse extends ActionResponse implements ToXContent {
private boolean created;
public PutRoleMappingResponse() {
}
public PutRoleMappingResponse(boolean created) {
this.created = created;
}
public boolean isCreated() {
return created;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject().field("created", created).endObject();
return builder;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeBoolean(created);
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
this.created = in.readBoolean();
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.rolemapping;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
public class TransportDeleteRoleMappingAction
extends HandledTransportAction<DeleteRoleMappingRequest, DeleteRoleMappingResponse> {
private final NativeRoleMappingStore roleMappingStore;
@Inject
public TransportDeleteRoleMappingAction(Settings settings, ThreadPool threadPool,
ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver,
TransportService transportService,
NativeRoleMappingStore roleMappingStore) {
super(settings, DeleteRoleMappingAction.NAME, threadPool, transportService, actionFilters,
indexNameExpressionResolver, DeleteRoleMappingRequest::new);
this.roleMappingStore = roleMappingStore;
}
@Override
protected void doExecute(DeleteRoleMappingRequest request,
ActionListener<DeleteRoleMappingResponse> listener) {
roleMappingStore.deleteRoleMapping(request, new ActionListener<Boolean>() {
@Override
public void onResponse(Boolean found) {
listener.onResponse(new DeleteRoleMappingResponse(found));
}
@Override
public void onFailure(Exception t) {
listener.onFailure(t);
}
});
}
}

View File

@ -0,0 +1,58 @@
/*
* 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.rolemapping;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
public class TransportGetRoleMappingsAction
extends HandledTransportAction<GetRoleMappingsRequest, GetRoleMappingsResponse> {
private final NativeRoleMappingStore roleMappingStore;
@Inject
public TransportGetRoleMappingsAction(Settings settings, ThreadPool threadPool,
ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver,
TransportService transportService,
NativeRoleMappingStore nativeRoleMappingStore) {
super(settings, GetRoleMappingsAction.NAME, threadPool, transportService, actionFilters,
indexNameExpressionResolver, GetRoleMappingsRequest::new);
this.roleMappingStore = nativeRoleMappingStore;
}
@Override
protected void doExecute(final GetRoleMappingsRequest request,
final ActionListener<GetRoleMappingsResponse> listener) {
final Set<String> names;
if (request.getNames() == null || request.getNames().length == 0) {
names = null;
} else {
names = new HashSet<>(Arrays.asList(request.getNames()));
}
this.roleMappingStore.getRoleMappings(names, ActionListener.wrap(
mappings -> {
ExpressionRoleMapping[] array = mappings.toArray(
new ExpressionRoleMapping[mappings.size()]
);
listener.onResponse(new GetRoleMappingsResponse(array));
},
listener::onFailure
));
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.rolemapping;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
public class TransportPutRoleMappingAction
extends HandledTransportAction<PutRoleMappingRequest, PutRoleMappingResponse> {
private final NativeRoleMappingStore roleMappingStore;
@Inject
public TransportPutRoleMappingAction(Settings settings, ThreadPool threadPool,
ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver,
TransportService transportService,
NativeRoleMappingStore roleMappingStore) {
super(settings, PutRoleMappingAction.NAME, threadPool, transportService, actionFilters,
indexNameExpressionResolver, PutRoleMappingRequest::new);
this.roleMappingStore = roleMappingStore;
}
@Override
protected void doExecute(final PutRoleMappingRequest request,
final ActionListener<PutRoleMappingResponse> listener) {
roleMappingStore.putRoleMapping(request, ActionListener.wrap(
created -> listener.onResponse(new PutRoleMappingResponse(created)),
listener::onFailure
));
}
}

View File

@ -14,13 +14,13 @@ import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.authc.file.FileRealm;
import org.elasticsearch.xpack.security.authc.ldap.LdapRealm;
import org.elasticsearch.xpack.security.authc.pki.PkiRealm;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.elasticsearch.xpack.ssl.SSLService;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -57,16 +57,20 @@ public class InternalRealms {
* This excludes the {@link ReservedRealm}, as it cannot be created dynamically.
* @return A map from <em>realm-type</em> to <code>Factory</code>
*/
public static Map<String, Realm.Factory> getFactories(ThreadPool threadPool, ResourceWatcherService resourceWatcherService,
SSLService sslService, NativeUsersStore nativeUsersStore){
public static Map<String, Realm.Factory> getFactories(
ThreadPool threadPool, ResourceWatcherService resourceWatcherService,
SSLService sslService, NativeUsersStore nativeUsersStore,
NativeRoleMappingStore nativeRoleMappingStore) {
Map<String, Realm.Factory> map = new HashMap<>();
map.put(FileRealm.TYPE, config -> new FileRealm(config, resourceWatcherService));
map.put(NativeRealm.TYPE, config -> new NativeRealm(config, nativeUsersStore));
map.put(LdapRealm.AD_TYPE,
config -> new LdapRealm(LdapRealm.AD_TYPE, config, resourceWatcherService, sslService, threadPool));
map.put(LdapRealm.LDAP_TYPE,
config -> new LdapRealm(LdapRealm.LDAP_TYPE, config, resourceWatcherService, sslService, threadPool));
map.put(PkiRealm.TYPE, config -> new PkiRealm(config, resourceWatcherService, sslService));
map.put(LdapRealm.AD_TYPE, config -> new LdapRealm(LdapRealm.AD_TYPE, config, sslService,
resourceWatcherService, nativeRoleMappingStore, threadPool));
map.put(LdapRealm.LDAP_TYPE, config -> new LdapRealm(LdapRealm.LDAP_TYPE, config,
sslService, resourceWatcherService, nativeRoleMappingStore, threadPool));
map.put(PkiRealm.TYPE, config -> new PkiRealm(config, sslService, resourceWatcherService,
nativeRoleMappingStore));
return Collections.unmodifiableMap(map);
}

View File

@ -34,6 +34,7 @@ import org.elasticsearch.index.engine.DocumentMissingException;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheRequest;
@ -77,7 +78,7 @@ public class NativeUsersStore extends AbstractComponent {
public NativeUsersStore(Settings settings, InternalClient client, SecurityLifecycleService securityLifecycleService) {
super(settings);
this.client = client;
this.isTribeNode = settings.getGroups("tribe", true).isEmpty() == false;
this.isTribeNode = XPackPlugin.isTribeNode(settings);
this.securityLifecycleService = securityLifecycleService;
}

View File

@ -21,6 +21,7 @@ import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession.GroupsResolver;
@ -71,14 +72,15 @@ class ActiveDirectorySessionFactory extends SessionFactory {
"] setting for active directory");
}
String domainDN = buildDnFromDomain(domainName);
GroupsResolver groupResolver = new ActiveDirectoryGroupsResolver(
settings.getAsSettings("group_search"), domainDN, ignoreReferralErrors);
defaultADAuthenticator = new DefaultADAuthenticator(settings, timeout,
ignoreReferralErrors, logger, groupResolver, domainDN);
downLevelADAuthenticator = new DownLevelADAuthenticator(config, timeout,
ignoreReferralErrors, logger, groupResolver, domainDN, sslService);
upnADAuthenticator = new UpnADAuthenticator(settings, timeout,
ignoreReferralErrors, logger, groupResolver, domainDN);
GroupsResolver groupResolver = new ActiveDirectoryGroupsResolver(settings.getAsSettings("group_search"), domainDN,
ignoreReferralErrors);
LdapMetaDataResolver metaDataResolver = new LdapMetaDataResolver(config.settings(), ignoreReferralErrors);
defaultADAuthenticator = new DefaultADAuthenticator(config, timeout, ignoreReferralErrors, logger, groupResolver,
metaDataResolver, domainDN);
downLevelADAuthenticator = new DownLevelADAuthenticator(config, timeout, ignoreReferralErrors, logger, groupResolver,
metaDataResolver, domainDN, sslService);
upnADAuthenticator = new UpnADAuthenticator(config, timeout, ignoreReferralErrors, logger, groupResolver,
metaDataResolver, domainDN);
}
@Override
@ -143,21 +145,26 @@ class ActiveDirectorySessionFactory extends SessionFactory {
abstract static class ADAuthenticator {
private final RealmConfig realm;
final TimeValue timeout;
final boolean ignoreReferralErrors;
final Logger logger;
final GroupsResolver groupsResolver;
final LdapMetaDataResolver metaDataResolver;
final String userSearchDN;
final LdapSearchScope userSearchScope;
final String userSearchFilter;
ADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors,
Logger logger, GroupsResolver groupsResolver, String domainDN, String userSearchFilterSetting,
String defaultUserSearchFilter) {
ADAuthenticator(RealmConfig realm, TimeValue timeout, boolean ignoreReferralErrors, Logger logger,
GroupsResolver groupsResolver, LdapMetaDataResolver metaDataResolver, String domainDN,
String userSearchFilterSetting, String defaultUserSearchFilter) {
this.realm = realm;
this.timeout = timeout;
this.ignoreReferralErrors = ignoreReferralErrors;
this.logger = logger;
this.groupsResolver = groupsResolver;
this.metaDataResolver = metaDataResolver;
final Settings settings = realm.settings();
userSearchDN = settings.get(AD_USER_SEARCH_BASEDN_SETTING, domainDN);
userSearchScope = LdapSearchScope.resolve(settings.get(AD_USER_SEARCH_SCOPE_SETTING), LdapSearchScope.SUB_TREE);
userSearchFilter = settings.get(userSearchFilterSetting, defaultUserSearchFilter);
@ -176,7 +183,8 @@ class ActiveDirectorySessionFactory extends SessionFactory {
+ "] by principle name yielded no results"));
} else {
final String dn = entry.getDN();
listener.onResponse(new LdapSession(logger, connection, dn, groupsResolver, timeout, null));
listener.onResponse(new LdapSession(logger, realm, connection, dn, groupsResolver, metaDataResolver,
timeout, null));
}
}, (e) -> {
IOUtils.closeWhileHandlingException(connection);
@ -213,12 +221,15 @@ class ActiveDirectorySessionFactory extends SessionFactory {
static class DefaultADAuthenticator extends ADAuthenticator {
final String domainName;
DefaultADAuthenticator(RealmConfig realm, TimeValue timeout, boolean ignoreReferralErrors,
Logger logger, GroupsResolver groupsResolver, LdapMetaDataResolver metaDataResolver, String domainDN) {
super(realm, timeout, ignoreReferralErrors, logger, groupsResolver, metaDataResolver, domainDN, AD_USER_SEARCH_FILTER_SETTING,
"(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0}@" + domainName(realm) + ")))");
domainName = domainName(realm);
}
DefaultADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors,
Logger logger, GroupsResolver groupsResolver, String domainDN) {
super(settings, timeout, ignoreReferralErrors, logger, groupsResolver, domainDN, AD_USER_SEARCH_FILTER_SETTING,
"(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0}@" + settings.get(AD_DOMAIN_NAME_SETTING) + ")))");
domainName = settings.get(AD_DOMAIN_NAME_SETTING);
private static String domainName(RealmConfig realm) {
return realm.settings().get(AD_DOMAIN_NAME_SETTING);
}
@Override
@ -253,11 +264,10 @@ class ActiveDirectorySessionFactory extends SessionFactory {
final SSLService sslService;
final RealmConfig config;
DownLevelADAuthenticator(RealmConfig config, TimeValue timeout,
boolean ignoreReferralErrors, Logger logger,
GroupsResolver groupsResolver, String domainDN,
DownLevelADAuthenticator(RealmConfig config, TimeValue timeout, boolean ignoreReferralErrors, Logger logger,
GroupsResolver groupsResolver, LdapMetaDataResolver metaDataResolver, String domainDN,
SSLService sslService) {
super(config.settings(), timeout, ignoreReferralErrors, logger, groupsResolver, domainDN,
super(config, timeout, ignoreReferralErrors, logger, groupsResolver, metaDataResolver, domainDN,
AD_DOWN_LEVEL_USER_SEARCH_FILTER_SETTING, DOWN_LEVEL_FILTER);
this.domainDN = domainDN;
this.settings = config.settings();
@ -388,9 +398,9 @@ class ActiveDirectorySessionFactory extends SessionFactory {
static final String UPN_USER_FILTER = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={1})))";
UpnADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors,
Logger logger, GroupsResolver groupsResolver, String domainDN) {
super(settings, timeout, ignoreReferralErrors, logger, groupsResolver, domainDN,
UpnADAuthenticator(RealmConfig config, TimeValue timeout, boolean ignoreReferralErrors, Logger logger,
GroupsResolver groupsResolver, LdapMetaDataResolver metaDataResolver, String domainDN) {
super(config, timeout, ignoreReferralErrors, logger, groupsResolver, metaDataResolver, domainDN,
AD_UPN_USER_SEARCH_FILTER_SETTING, UPN_USER_FILTER);
}

View File

@ -9,6 +9,7 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import com.unboundid.ldap.sdk.LDAPException;
@ -31,11 +32,15 @@ import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.RealmSettings;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapLoadBalancing;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession;
import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory;
import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.xpack.security.authc.support.DnRoleMapper;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.elasticsearch.xpack.security.user.User;
import org.elasticsearch.xpack.ssl.SSLService;
@ -51,26 +56,34 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
Setting.timeSetting("timeout.execution", TimeValue.timeValueSeconds(30L), Property.NodeScope);
private final SessionFactory sessionFactory;
private final DnRoleMapper roleMapper;
private final UserRoleMapper roleMapper;
private final ThreadPool threadPool;
private final TimeValue executionTimeout;
public LdapRealm(String type, RealmConfig config, ResourceWatcherService watcherService, SSLService sslService,
ThreadPool threadPool) throws LDAPException {
this(type, config, sessionFactory(config, sslService, type), new DnRoleMapper(type, config, watcherService), threadPool);
public LdapRealm(String type, RealmConfig config, SSLService sslService,
ResourceWatcherService watcherService,
NativeRoleMappingStore nativeRoleMappingStore, ThreadPool threadPool)
throws LDAPException {
this(type, config, sessionFactory(config, sslService, type),
new CompositeRoleMapper(type, config, watcherService, nativeRoleMappingStore),
threadPool);
}
// pkg private for testing
LdapRealm(String type, RealmConfig config, SessionFactory sessionFactory, DnRoleMapper roleMapper, ThreadPool threadPool) {
LdapRealm(String type, RealmConfig config, SessionFactory sessionFactory,
UserRoleMapper roleMapper, ThreadPool threadPool) {
super(type, config);
this.sessionFactory = sessionFactory;
this.roleMapper = roleMapper;
this.threadPool = threadPool;
this.executionTimeout = EXECUTION_TIMEOUT.get(config.settings());
roleMapper.addListener(this::expireAll);
roleMapper.refreshRealmOnChange(this);
}
static SessionFactory sessionFactory(RealmConfig config, SSLService sslService, String type) throws LDAPException {
static SessionFactory sessionFactory(RealmConfig config, SSLService sslService, String type)
throws LDAPException {
final SessionFactory sessionFactory;
if (AD_TYPE.equals(type)) {
sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
@ -105,13 +118,13 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
}
/**
* @return The {@link Setting setting configuration} for this realm type
* @param type Either {@link #AD_TYPE} or {@link #LDAP_TYPE}
* @return The {@link Setting setting configuration} for this realm type
*/
public static Set<Setting<?>> getSettings(String type) {
Set<Setting<?>> settings = new HashSet<>();
settings.addAll(CachingUsernamePasswordRealm.getCachingSettings());
DnRoleMapper.getSettings(settings);
settings.addAll(CompositeRoleMapper.getSettings());
settings.add(EXECUTION_TIMEOUT);
if (AD_TYPE.equals(type)) {
settings.addAll(ActiveDirectorySessionFactory.getSettings());
@ -120,6 +133,7 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
settings.addAll(LdapSessionFactory.getSettings());
settings.addAll(LdapUserSearchSessionFactory.getSettings());
}
settings.addAll(LdapMetaDataResolver.getSettings());
return settings;
}
@ -174,25 +188,34 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
return usage;
}
private static void lookupGroups(LdapSession session, String username, ActionListener<User> listener, DnRoleMapper roleMapper) {
private static void buildUser(LdapSession session, String username, ActionListener<User> listener, UserRoleMapper roleMapper) {
if (session == null) {
listener.onResponse(null);
} else {
boolean loadingGroups = false;
try {
session.groups(ActionListener.wrap((groups) -> {
Set<String> roles = roleMapper.resolveRoles(session.userDn(), groups);
IOUtils.close(session);
final Map<String, Object> meta = MapBuilder.<String, Object>newMapBuilder()
.put("ldap_dn", session.userDn())
.put("ldap_groups", groups)
.map();
listener.onResponse(new User(username, roles.toArray(Strings.EMPTY_ARRAY), null, null, meta, true));
},
(e) -> {
IOUtils.closeWhileHandlingException(session);
listener.onFailure(e);
}));
final Consumer<Exception> onFailure = e -> {
IOUtils.closeWhileHandlingException(session);
listener.onFailure(e);
};
session.resolve(ActionListener.wrap((ldapData) -> {
final Map<String, Object> metadata = MapBuilder.<String, Object>newMapBuilder()
.put("ldap_dn", session.userDn())
.put("ldap_groups", ldapData.groups)
.putAll(ldapData.metaData)
.map();
final UserData user = new UserData(username, session.userDn(), ldapData.groups,
metadata, session.realm());
roleMapper.resolveRoles(user, ActionListener.wrap(
roles -> {
IOUtils.close(session);
String[] rolesArray = roles.toArray(new String[roles.size()]);
listener.onResponse(
new User(username, rolesArray, null, null, metadata, true)
);
}, onFailure
));
}, onFailure));
loadingGroups = true;
} finally {
if (loadingGroups == false) {
@ -227,7 +250,7 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
userActionListener.onResponse(null);
} else {
ldapSessionAtomicReference.set(session);
lookupGroups(session, username, userActionListener, roleMapper);
buildUser(session, username, userActionListener, roleMapper);
}
}

View File

@ -18,6 +18,7 @@ import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.RealmSettings;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession.GroupsResolver;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils;
@ -49,6 +50,7 @@ public class LdapSessionFactory extends SessionFactory {
private final String[] userDnTemplates;
private final GroupsResolver groupResolver;
private final LdapMetaDataResolver metaDataResolver;
public LdapSessionFactory(RealmConfig config, SSLService sslService) {
super(config, sslService);
@ -60,6 +62,7 @@ public class LdapSessionFactory extends SessionFactory {
}
logger.info("Realm [{}] is in user-dn-template mode: [{}]", config.name(), userDnTemplates);
groupResolver = groupResolver(settings);
metaDataResolver = new LdapMetaDataResolver(settings, ignoreReferralErrors);
}
/**
@ -81,7 +84,7 @@ public class LdapSessionFactory extends SessionFactory {
String dn = buildDnFromTemplate(username, template);
try {
connection.bind(new SimpleBindRequest(dn, passwordBytes));
ldapSession = new LdapSession(logger, connection, dn, groupResolver, timeout, null);
ldapSession = new LdapSession(logger, config, connection, dn, groupResolver, metaDataResolver, timeout, null);
success = true;
break;
} catch (LDAPException e) {

View File

@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.RealmSettings;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession.GroupsResolver;
@ -81,6 +82,7 @@ class LdapUserSearchSessionFactory extends SessionFactory {
private final boolean useConnectionPool;
private final LDAPConnectionPool connectionPool;
private final LdapMetaDataResolver metaDataResolver;
LdapUserSearchSessionFactory(RealmConfig config, SSLService sslService) throws LDAPException {
super(config, sslService);
@ -93,6 +95,7 @@ class LdapUserSearchSessionFactory extends SessionFactory {
scope = SEARCH_SCOPE.get(settings);
userAttribute = SEARCH_ATTRIBUTE.get(settings);
groupResolver = groupResolver(settings);
metaDataResolver = new LdapMetaDataResolver(config.settings(), ignoreReferralErrors);
useConnectionPool = POOL_ENABLED.get(settings);
if (useConnectionPool) {
connectionPool = createConnectionPool(config, serverSet, timeout, logger);
@ -175,7 +178,8 @@ class LdapUserSearchSessionFactory extends SessionFactory {
final byte[] passwordBytes = CharArrays.toUtf8Bytes(password.getChars());
try {
LdapUtils.privilegedConnect(() -> connectionPool.bindAndRevertAuthentication(new SimpleBindRequest(dn, passwordBytes)));
listener.onResponse(new LdapSession(logger, connectionPool, dn, groupResolver, timeout, entry.getAttributes()));
listener.onResponse(new LdapSession(logger, config, connectionPool, dn, groupResolver, metaDataResolver, timeout,
entry.getAttributes()));
} catch (LDAPException e) {
listener.onFailure(e);
} finally {
@ -218,8 +222,8 @@ class LdapUserSearchSessionFactory extends SessionFactory {
try {
userConnection = LdapUtils.privilegedConnect(serverSet::getConnection);
userConnection.bind(new SimpleBindRequest(dn, passwordBytes));
LdapSession session = new LdapSession(logger, userConnection, dn, groupResolver, timeout,
entry.getAttributes());
LdapSession session = new LdapSession(logger, config, userConnection, dn, groupResolver,
metaDataResolver, timeout, entry.getAttributes());
sessionCreated = true;
listener.onResponse(session);
} catch (Exception e) {
@ -273,7 +277,8 @@ class LdapUserSearchSessionFactory extends SessionFactory {
boolean sessionCreated = false;
try {
final String dn = entry.getDN();
LdapSession session = new LdapSession(logger, ldapInterface, dn, groupResolver, timeout, entry.getAttributes());
LdapSession session = new LdapSession(logger, config, ldapInterface, dn, groupResolver, metaDataResolver, timeout,
entry.getAttributes());
sessionCreated = true;
listener.onResponse(session);
} finally {
@ -295,9 +300,8 @@ class LdapUserSearchSessionFactory extends SessionFactory {
private void findUser(String user, LDAPInterface ldapInterface, ActionListener<SearchResultEntry> listener) {
searchForEntry(ldapInterface, userSearchBaseDn, scope.scope(),
createEqualityFilter(userAttribute, encodeValue(user)),
Math.toIntExact(timeout.seconds()), ignoreReferralErrors, listener,
attributesToSearchFor(groupResolver.attributes()));
createEqualityFilter(userAttribute, encodeValue(user)), Math.toIntExact(timeout.seconds()), ignoreReferralErrors, listener,
attributesToSearchFor(groupResolver.attributes(), metaDataResolver.attributeNames()));
}
/*

View File

@ -0,0 +1,93 @@
/*
* 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.authc.ldap.support;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPInterface;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.OBJECT_CLASS_PRESENCE_FILTER;
import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.searchForEntry;
public class LdapMetaDataResolver {
public static final Setting<List<String>> ADDITIONAL_META_DATA_SETTING = Setting.listSetting(
"meta_data", Collections.emptyList(), Function.identity(), Setting.Property.NodeScope);
private final String[] attributeNames;
private final boolean ignoreReferralErrors;
public LdapMetaDataResolver(Settings settings, boolean ignoreReferralErrors) {
this(ADDITIONAL_META_DATA_SETTING.get(settings), ignoreReferralErrors);
}
LdapMetaDataResolver(Collection<String> attributeNames, boolean ignoreReferralErrors) {
this.attributeNames = attributeNames.toArray(new String[attributeNames.size()]);
this.ignoreReferralErrors = ignoreReferralErrors;
}
public String[] attributeNames() {
return attributeNames;
}
public void resolve(LDAPInterface connection, String userDn, TimeValue timeout, Logger logger,
Collection<Attribute> attributes,
ActionListener<Map<String, Object>> listener) {
if (this.attributeNames.length == 0) {
listener.onResponse(Collections.emptyMap());
} else if (attributes != null) {
listener.onResponse(toMap(name -> findAttribute(attributes, name)));
} else {
searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER,
Math.toIntExact(timeout.seconds()), ignoreReferralErrors,
ActionListener.wrap((SearchResultEntry entry) -> {
if (entry == null) {
listener.onResponse(Collections.emptyMap());
} else {
listener.onResponse(toMap(entry::getAttribute));
}
}, listener::onFailure), this.attributeNames);
}
}
private Attribute findAttribute(Collection<Attribute> attributes, String name) {
return attributes.stream()
.filter(attr -> attr.getName().equals(name))
.findFirst().orElse(null);
}
private Map<String, Object> toMap(Function<String, Attribute> attributes) {
return Collections.unmodifiableMap(
Arrays.stream(this.attributeNames).map(attributes).filter(Objects::nonNull)
.collect(Collectors.toMap(
attr -> attr.getName(),
attr -> {
final String[] values = attr.getValues();
return values.length == 1 ? values[0] : Arrays.asList(values);
})
)
);
}
public static List<Setting<?>> getSettings() {
return Collections.singletonList(ADDITIONAL_META_DATA_SETTING);
}
}

View File

@ -12,9 +12,12 @@ import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.lease.Releasable;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.ldap.LdapRealm;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Represents a LDAP connection with an authenticated/bound user that needs closing.
@ -22,9 +25,11 @@ import java.util.List;
public class LdapSession implements Releasable {
protected final Logger logger;
protected final RealmConfig realm;
protected final LDAPInterface ldap;
protected final String userDn;
protected final GroupsResolver groupsResolver;
private LdapMetaDataResolver metaDataResolver;
protected final TimeValue timeout;
protected final Collection<Attribute> attributes;
@ -36,12 +41,14 @@ public class LdapSession implements Releasable {
* outside of and be reused across all connections. We can't keep a static logger in this class
* since we want the logger to be contextual (i.e. aware of the settings and its environment).
*/
public LdapSession(Logger logger, LDAPInterface connection, String userDn, GroupsResolver groupsResolver, TimeValue timeout,
Collection<Attribute> attributes) {
public LdapSession(Logger logger, RealmConfig realm, LDAPInterface connection, String userDn, GroupsResolver groupsResolver,
LdapMetaDataResolver metaDataResolver, TimeValue timeout, Collection<Attribute> attributes) {
this.logger = logger;
this.realm = realm;
this.ldap = connection;
this.userDn = userDn;
this.groupsResolver = groupsResolver;
this.metaDataResolver = metaDataResolver;
this.timeout = timeout;
this.attributes = attributes;
}
@ -65,6 +72,13 @@ public class LdapSession implements Releasable {
return userDn;
}
/**
* @return the realm for which this session was created
*/
public RealmConfig realm() {
return realm;
}
/**
* Asynchronously retrieves a list of group distinguished names
*/
@ -72,6 +86,35 @@ public class LdapSession implements Releasable {
groupsResolver.resolve(ldap, userDn, timeout, logger, attributes, listener);
}
public void metaData(ActionListener<Map<String, Object>> listener) {
metaDataResolver.resolve(ldap, userDn, timeout, logger, attributes, listener);
}
public void resolve(ActionListener<LdapUserData> listener) {
logger.debug("Resolving LDAP groups + meta-data for user [{}]", userDn);
groups(ActionListener.wrap(
groups -> {
logger.debug("Resolved {} LDAP groups [{}] for user [{}]", groups.size(), groups, userDn);
metaData(ActionListener.wrap(
meta -> {
logger.debug("Resolved {} meta-data fields [{}] for user [{}]", meta.size(), meta, userDn);
listener.onResponse(new LdapUserData(groups, meta));
},
listener::onFailure));
},
listener::onFailure));
}
public static class LdapUserData {
public final List<String> groups;
public final Map<String, Object> metaData;
public LdapUserData(List<String> groups, Map<String, Object> metaData) {
this.groups = groups;
this.metaData = metaData;
}
}
/**
* A GroupsResolver is used to resolve the group names of a given LDAP user
*/

View File

@ -309,6 +309,17 @@ public final class LdapUtils {
return attributes == null ? new String[] { SearchRequest.NO_ATTRIBUTES } : attributes;
}
public static String[] attributesToSearchFor(String[]... args) {
List<String> attributes = new ArrayList<>();
for (String[] array : args) {
if (array != null) {
attributes.addAll(Arrays.asList(array));
}
}
return attributes.isEmpty() ? attributesToSearchFor((String[]) null)
: attributes.toArray(new String[attributes.size()]);
}
static String[] encodeFilterValues(String... arguments) {
for (int i = 0; i < arguments.length; i++) {
arguments[i] = Filter.encodeValue(arguments[i]);

View File

@ -17,6 +17,9 @@ import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.elasticsearch.xpack.security.transport.netty4.SecurityNetty4Transport;
import org.elasticsearch.xpack.ssl.CertUtils;
import org.elasticsearch.xpack.ssl.SSLConfigurationSettings;
@ -26,7 +29,6 @@ import org.elasticsearch.xpack.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.security.authc.Realm;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.RealmSettings;
import org.elasticsearch.xpack.security.authc.support.DnRoleMapper;
import javax.net.ssl.X509TrustManager;
import java.security.cert.Certificate;
@ -58,15 +60,18 @@ public class PkiRealm extends Realm {
private final X509TrustManager trustManager;
private final Pattern principalPattern;
private final DnRoleMapper roleMapper;
private final UserRoleMapper roleMapper;
public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, SSLService sslService) {
this(config, new DnRoleMapper(TYPE, config, watcherService), sslService);
public PkiRealm(RealmConfig config, SSLService sslService,
ResourceWatcherService watcherService,
NativeRoleMappingStore nativeRoleMappingStore) {
this(config, new CompositeRoleMapper(TYPE, config, watcherService, nativeRoleMappingStore),
sslService);
}
// pkg private for testing
PkiRealm(RealmConfig config, DnRoleMapper roleMapper, SSLService sslService) {
PkiRealm(RealmConfig config, UserRoleMapper roleMapper, SSLService sslService) {
super(TYPE, config);
this.trustManager = trustManagers(config);
this.principalPattern = USERNAME_PATTERN_SETTING.get(config.settings());
@ -90,8 +95,14 @@ public class PkiRealm extends Realm {
if (isCertificateChainTrusted(trustManager, token, logger) == false) {
listener.onResponse(null);
} else {
Set<String> roles = roleMapper.resolveRoles(token.dn(), Collections.<String>emptyList());
listener.onResponse(new User(token.principal(), roles.toArray(new String[roles.size()])));
final Map<String, Object> metadata = Collections.singletonMap("pki_dn", token.dn());
final UserRoleMapper.UserData user = new UserRoleMapper.UserData(token.principal(),
token.dn(), Collections.emptySet(), metadata, this.config);
roleMapper.resolveRoles(user, ActionListener.wrap(
roles -> listener.onResponse(new User(token.principal(),
roles.toArray(new String[roles.size()]), null, null, metadata, true)),
listener::onFailure
));
}
}
@ -242,7 +253,7 @@ public class PkiRealm extends Realm {
settings.add(SSL_SETTINGS.truststoreAlgorithm);
settings.add(SSL_SETTINGS.caPaths);
DnRoleMapper.getSettings(settings);
settings.addAll(CompositeRoleMapper.getSettings());
return settings;
}

View File

@ -11,6 +11,7 @@ import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
@ -24,6 +25,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -42,7 +45,7 @@ import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.rela
/**
* This class loads and monitors the file defining the mappings of DNs to internal ES Roles.
*/
public class DnRoleMapper {
public class DnRoleMapper implements UserRoleMapper {
private static final String DEFAULT_FILE_NAME = "role_mapping.yml";
public static final Setting<String> ROLE_MAPPING_FILE_SETTING = new Setting<>("files.role_mapping", DEFAULT_FILE_NAME,
@ -77,7 +80,12 @@ public class DnRoleMapper {
}
}
public synchronized void addListener(Runnable listener) {
@Override
public void refreshRealmOnChange(CachingUsernamePasswordRealm realm) {
addListener(realm::expireAll);
}
synchronized void addListener(Runnable listener) {
listeners.add(Objects.requireNonNull(listener, "listener cannot be null"));
}
@ -113,9 +121,7 @@ public class DnRoleMapper {
}
try (InputStream in = Files.newInputStream(path)) {
Settings settings = Settings.builder()
.loadFromStream(path.toString(), in)
.build();
Settings settings = Settings.builder().loadFromStream(path.toString(), in).build();
Map<DN, Set<String>> dnToRoles = new HashMap<>();
Set<String> roles = settings.names();
@ -130,8 +136,7 @@ public class DnRoleMapper {
}
dnRoles.add(role);
} catch (LDAPException e) {
logger.error(
(Supplier<?>) () -> new ParameterizedMessage(
logger.error(new ParameterizedMessage(
"invalid DN [{}] found in [{}] role mappings [{}] for realm [{}/{}]. skipping... ",
providedDn,
realmType,
@ -157,10 +162,19 @@ public class DnRoleMapper {
return dnRoles.size();
}
@Override
public void resolveRoles(UserData user, ActionListener<Set<String>> listener) {
try {
listener.onResponse(resolveRoles(user.getDn(), user.getGroups()));
} catch( Exception e) {
listener.onFailure(e);
}
}
/**
* This will map the groupDN's to ES Roles
*/
public Set<String> resolveRoles(String userDnString, List<String> groupDns) {
public Set<String> resolveRoles(String userDnString, Collection<String> groupDns) {
Set<String> roles = new HashSet<>();
for (String groupDnString : groupDns) {
DN groupDn = dn(groupDnString);
@ -171,8 +185,8 @@ public class DnRoleMapper {
}
}
if (logger.isDebugEnabled()) {
logger.debug("the roles [{}], are mapped from these [{}] groups [{}] for realm [{}/{}]", roles, realmType, groupDns,
realmType, config.name());
logger.debug("the roles [{}], are mapped from these [{}] groups [{}] using file [{}] for realm [{}/{}]", roles, realmType,
groupDns, file.getFileName(), realmType, config.name());
}
DN userDn = dn(userDnString);
@ -181,8 +195,9 @@ public class DnRoleMapper {
roles.addAll(rolesMappedToUserDn);
}
if (logger.isDebugEnabled()) {
logger.debug("the roles [{}], are mapped from the user [{}] for realm [{}/{}]",
(rolesMappedToUserDn == null) ? Collections.emptySet() : rolesMappedToUserDn, userDnString, realmType, config.name());
logger.debug("the roles [{}], are mapped from the user [{}] using file [{}] for realm [{}/{}]",
(rolesMappedToUserDn == null) ? Collections.emptySet() : rolesMappedToUserDn, userDnString, file.getFileName(),
realmType, config.name());
}
return roles;
}
@ -191,9 +206,8 @@ public class DnRoleMapper {
listeners.forEach(Runnable::run);
}
public static void getSettings(Set<Setting<?>> settings) {
settings.add(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING);
settings.add(ROLE_MAPPING_FILE_SETTING);
public static List<Setting<?>> getSettings() {
return Arrays.asList(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, ROLE_MAPPING_FILE_SETTING);
}
private class FileListener implements FileChangesListener {

View File

@ -0,0 +1,128 @@
/*
* 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.authc.support;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.xpack.security.authc.RealmConfig;
/**
* Where a realm users an authentication method that does not have in-built support for X-Pack
* {@link org.elasticsearch.xpack.security.authz.permission.Role roles}, it may delegate to an implementation of this class the
* responsibility for determining the set roles that an authenticated user should have.
*/
public interface UserRoleMapper {
/**
* Determines the set of roles that should be applied to <code>user</code>.
*/
void resolveRoles(UserData user, ActionListener<Set<String>> listener);
/**
* Informs the mapper that the provided <code>realm</code> should be refreshed when
* the set of role-mappings change. The realm may be updated for the local node only, or across
* the whole cluster depending on whether this role-mapper has node-local data or cluster-wide
* data.
*/
void refreshRealmOnChange(CachingUsernamePasswordRealm realm);
/**
* A representation of a user for whom roles should be mapped.
* The user has been authenticated, but does not yet have any roles.
*/
class UserData {
private final String username;
@Nullable
private final String dn;
private final Set<String> groups;
private final Map<String, Object> metadata;
private final RealmConfig realm;
public UserData(String username, @Nullable String dn, Collection<String> groups,
Map<String, Object> metadata, RealmConfig realm) {
this.username = username;
this.dn = dn;
this.groups = groups == null || groups.isEmpty()
? Collections.emptySet() : Collections.unmodifiableSet(new HashSet<>(groups));
this.metadata = metadata == null || metadata.isEmpty()
? Collections.emptyMap() : Collections.unmodifiableMap(metadata);
this.realm = realm;
}
/**
* Formats the user data as a <code>Map</code>.
* The map is <em>not</em> nested - all values are simple Java values, but keys may
* contain <code>.</code>.
* For example, the {@link #metadata} values will be stored in the map with a key of
* <code>"metadata.KEY"</code> where <code>KEY</code> is the key from the metadata object.
*/
public Map<String, Object> asMap() {
final Map<String, Object> map = new HashMap<>();
map.put("username", username);
map.put("dn", dn);
map.put("groups", groups);
metadata.keySet().forEach(k -> map.put("metadata." + k, metadata.get(k)));
map.put("realm.name", realm.name());
return map;
}
@Override
public String toString() {
return "UserData{" +
"username:" + username +
"; dn:" + dn +
"; groups:" + groups +
"; metadata:" + metadata +
"; realm=" + realm.name() +
'}';
}
/**
* The username for the authenticated user.
*/
public String getUsername() {
return username;
}
/**
* The <em>distinguished name</em> of the authenticated user, if applicable to the
* authentication method used. Otherwise, <code>null</code>.
*/
@Nullable
public String getDn() {
return dn;
}
/**
* The groups to which the user belongs in the originating user store. Should be empty
* if the user store or authentication method does not support groups.
*/
public Set<String> getGroups() {
return groups;
}
/**
* Any additional metadata that was provided at authentication time. The set of keys will
* vary according to the authenticating realm.
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* The realm that authenticated the user.
*/
public RealmConfig getRealm() {
return realm;
}
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.authc.support.mapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.GroupedActionListener;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.xpack.security.authc.support.DnRoleMapper;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
/**
* A {@link UserRoleMapper} that composes one or more <i>delegate</i> role-mappers.
* During {@link #resolveRoles(UserData, ActionListener) role resolution}, each of the delegates is
* queried, and the individual results are merged into a single {@link Set} which includes all the roles from each mapper.
*/
public class CompositeRoleMapper implements UserRoleMapper {
private List<UserRoleMapper> delegates;
public CompositeRoleMapper(String realmType, RealmConfig realmConfig,
ResourceWatcherService watcherService,
NativeRoleMappingStore nativeRoleMappingStore) {
this(new DnRoleMapper(realmType, realmConfig, watcherService), nativeRoleMappingStore);
}
private CompositeRoleMapper(UserRoleMapper... delegates) {
this.delegates = new ArrayList<>(Arrays.asList(delegates));
}
@Override
public void resolveRoles(UserData user, ActionListener<Set<String>> listener) {
GroupedActionListener<Set<String>> groupListener = new GroupedActionListener<>(ActionListener.wrap(
composite -> listener.onResponse(composite.stream().flatMap(Set::stream).collect(Collectors.toSet())), listener::onFailure
), delegates.size(), Collections.emptyList());
this.delegates.forEach(mapper -> mapper.resolveRoles(user, groupListener));
}
@Override
public void refreshRealmOnChange(CachingUsernamePasswordRealm realm) {
this.delegates.forEach(mapper -> mapper.refreshRealmOnChange(realm));
}
public static Collection<? extends Setting<?>> getSettings() {
return DnRoleMapper.getSettings();
}
}

View File

@ -0,0 +1,236 @@
/*
* 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.authc.support.mapper;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.ExpressionParser;
/**
* A representation of a single role-mapping for use in {@link NativeRoleMappingStore}.
* Logically, this represents a set of roles that should be applied to any user where a boolean
* expression evaluates to <code>true</code>.
*
* @see RoleMapperExpression
* @see ExpressionParser
*/
public class ExpressionRoleMapping implements ToXContentObject, Writeable {
private static final ObjectParser<Builder, String> PARSER = new ObjectParser<>("role-mapping", Builder::new);
static {
PARSER.declareStringArray(Builder::roles, Fields.ROLES);
PARSER.declareField(Builder::rules, ExpressionParser::parseObject, Fields.RULES, ObjectParser.ValueType.OBJECT);
PARSER.declareField(Builder::metadata, XContentParser::map, Fields.METADATA, ObjectParser.ValueType.OBJECT);
PARSER.declareBoolean(Builder::enabled, Fields.ENABLED);
BiConsumer<Builder, String> ignored = (b, v) -> {
};
// skip the doc_type field in case we're parsing directly from the index
PARSER.declareString(ignored, new ParseField(NativeRoleMappingStore.DOC_TYPE_FIELD));
}
private final String name;
private final RoleMapperExpression expression;
private final List<String> roles;
private final Map<String, Object> metadata;
private final boolean enabled;
public ExpressionRoleMapping(String name, RoleMapperExpression expr, List<String> roles, Map<String, Object> metadata,
boolean enabled) {
this.name = name;
this.expression = expr;
this.roles = roles;
this.metadata = metadata;
this.enabled = enabled;
}
public ExpressionRoleMapping(StreamInput in) throws IOException {
this.name = in.readString();
this.enabled = in.readBoolean();
this.roles = in.readList(StreamInput::readString);
this.expression = ExpressionParser.readExpression(in);
this.metadata = in.readMap();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(name);
out.writeBoolean(enabled);
out.writeStringList(roles);
ExpressionParser.writeExpression(expression, out);
out.writeMap(metadata);
}
/**
* The name of this mapping. The name exists for the sole purpose of providing a meaningful identifier for each mapping, so that it may
* be referred to for update, retrieval or deletion. The name does not affect the set of roles that a mapping provides.
*/
public String getName() {
return name;
}
/**
* The expression that determines whether the roles in this mapping should be applied to any given user.
* If the expression {@link RoleMapperExpression#match(Map) matches} a
* {@link org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData user}, then the user should be assigned this mapping's
* {@link #getRoles() roles}
*/
public RoleMapperExpression getExpression() {
return expression;
}
/**
* The list of {@link org.elasticsearch.xpack.security.authz.RoleDescriptor roles} (specified by name) that should be assigned to users
* that match the {@link #getExpression() expression} in this mapping.
*/
public List<String> getRoles() {
return Collections.unmodifiableList(roles);
}
/**
* Meta-data for this mapping. This exists for external systems of user to track information about this mapping such as where it was
* sourced from, when it was loaded, etc.
* This is not used within the mapping process, and does not affect whether the expression matches, nor which roles are assigned.
*/
public Map<String, Object> getMetadata() {
return Collections.unmodifiableMap(metadata);
}
/**
* Whether this mapping is enabled. Mappings that are not enabled are not applied to users.
*/
public boolean isEnabled() {
return enabled;
}
@Override
public String toString() {
return getClass().getSimpleName() + "<" + name + " ; " + roles + " = " + expression + ">";
}
/**
* Parse an {@link ExpressionRoleMapping} from the provided <em>XContent</em>
*/
public static ExpressionRoleMapping parse(String name, BytesReference source, XContentType xContentType) throws IOException {
final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY;
try (XContentParser parser = xContentType.xContent().createParser(registry, source)) {
return parse(name, parser);
}
}
/**
* Parse an {@link ExpressionRoleMapping} from the provided <em>XContent</em>
*/
public static ExpressionRoleMapping parse(String name, XContentParser parser) throws IOException {
try {
final Builder builder = PARSER.parse(parser, null);
return builder.build(name);
} catch (IllegalArgumentException | IllegalStateException e) {
throw new ParsingException(parser.getTokenLocation(), e.getMessage(), e);
}
}
/**
* Converts this {@link ExpressionRoleMapping} into <em>XContent</em> that is compatible with
* the format handled by {@link #parse(String, XContentParser)}.
*/
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return toXContent(builder, params, false);
}
XContentBuilder toXContent(XContentBuilder builder, Params params, boolean includeDocType) throws IOException {
builder.startObject();
builder.field(Fields.ENABLED.getPreferredName(), enabled);
builder.startArray(Fields.ROLES.getPreferredName());
for (String r : roles) {
builder.value(r);
}
builder.endArray();
builder.field(Fields.RULES.getPreferredName());
expression.toXContent(builder, params);
builder.field(Fields.METADATA.getPreferredName(), metadata);
if (includeDocType) {
builder.field(NativeRoleMappingStore.DOC_TYPE_FIELD, NativeRoleMappingStore.DOC_TYPE_ROLE_MAPPING);
}
return builder.endObject();
}
/**
* Used to facilitate the use of {@link ObjectParser} (via {@link #PARSER}).
*/
private static class Builder {
private RoleMapperExpression rules;
private List<String> roles;
private Map<String, Object> metadata = Collections.emptyMap();
private Boolean enabled;
Builder rules(RoleMapperExpression expression) {
this.rules = expression;
return this;
}
Builder roles(List<String> roles) {
this.roles = roles;
return this;
}
Builder metadata(Map<String, Object> metadata) {
this.metadata = metadata;
return this;
}
Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
private ExpressionRoleMapping build(String name) {
if (roles == null) {
throw missingField(name, Fields.ROLES);
}
if (rules == null) {
throw missingField(name, Fields.RULES);
}
if (enabled == null) {
throw missingField(name, Fields.ENABLED);
}
return new ExpressionRoleMapping(name, rules, roles, metadata, enabled);
}
private IllegalStateException missingField(String id, ParseField field) {
return new IllegalStateException("failed to parse role-mapping [" + id + "]. missing field [" + field + "]");
}
}
public interface Fields {
ParseField ROLES = new ParseField("roles");
ParseField ENABLED = new ParseField("enabled");
ParseField RULES = new ParseField("rules");
ParseField METADATA = new ParseField("metadata");
}
}

View File

@ -0,0 +1,313 @@
/*
* 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.authc.support.mapper;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.common.CheckedBiConsumer;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.action.rolemapping.DeleteRoleMappingRequest;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingRequest;
import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.security.client.SecurityClient;
import static org.elasticsearch.action.DocWriteResponse.Result.CREATED;
import static org.elasticsearch.action.DocWriteResponse.Result.DELETED;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
/**
* This store reads + writes {@link ExpressionRoleMapping role mappings} in an Elasticsearch
* {@link SecurityLifecycleService#SECURITY_INDEX_NAME index}.
* <br>
* The store is responsible for all read and write operations as well as
* {@link #resolveRoles(UserData, ActionListener) resolving roles}.
* <p>
* No caching is done by this class, it is handled at a higher level and no polling for changes
* is done by this class. Modification operations make a best effort attempt to clear the cache
* on all nodes for the user that was modified.
*/
public class NativeRoleMappingStore extends AbstractComponent implements UserRoleMapper {
static final String DOC_TYPE_FIELD = "doc_type";
static final String DOC_TYPE_ROLE_MAPPING = "role-mapping";
private static final String ID_PREFIX = DOC_TYPE_ROLE_MAPPING + "_";
private static final String SECURITY_GENERIC_TYPE = "doc";
private final InternalClient client;
private final boolean isTribeNode;
private final SecurityLifecycleService securityLifecycleService;
private final List<String> realmsToRefresh = new CopyOnWriteArrayList<>();
public NativeRoleMappingStore(Settings settings, InternalClient client, SecurityLifecycleService securityLifecycleService) {
super(settings);
this.client = client;
this.isTribeNode = XPackPlugin.isTribeNode(settings);
this.securityLifecycleService = securityLifecycleService;
}
private String getNameFromId(String id) {
assert id.startsWith(ID_PREFIX);
return id.substring(ID_PREFIX.length());
}
private String getIdForName(String name) {
return ID_PREFIX + name;
}
/**
* Loads all mappings from the index.
* <em>package private</em> for unit testing
*/
void loadMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
final QueryBuilder query = QueryBuilders.termQuery(DOC_TYPE_FIELD, DOC_TYPE_ROLE_MAPPING);
SearchRequest request = client.prepareSearch(SecurityLifecycleService.SECURITY_INDEX_NAME)
.setScroll(TimeValue.timeValueSeconds(10L))
.setTypes(SECURITY_GENERIC_TYPE)
.setQuery(query)
.setSize(1000)
.setFetchSource(true)
.request();
request.indicesOptions().ignoreUnavailable();
InternalClient.fetchAllByEntity(client, request, ActionListener.wrap((Collection<ExpressionRoleMapping> mappings) ->
listener.onResponse(mappings.stream().filter(Objects::nonNull).collect(Collectors.toList())),
ex -> {
logger.error(new ParameterizedMessage("failed to load role mappings from index [{}] skipping all mappings.",
SecurityLifecycleService.SECURITY_INDEX_NAME), ex);
listener.onResponse(Collections.emptyList());
}),
doc -> buildMapping(getNameFromId(doc.getId()), doc.getSourceRef()));
}
private ExpressionRoleMapping buildMapping(String id, BytesReference source) {
try (XContentParser parser = getParser(source)) {
return ExpressionRoleMapping.parse(id, parser);
} catch (Exception e) {
logger.warn(new ParameterizedMessage("Role mapping [{}] cannot be parsed and will be skipped", id), e);
return null;
}
}
private static XContentParser getParser(BytesReference source) throws IOException {
return XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, source);
}
/**
* Stores (create or update) a single mapping in the index
*/
public void putRoleMapping(PutRoleMappingRequest request, ActionListener<Boolean> listener) {
modifyMapping(request.getName(), this::innerPutMapping, request, listener);
}
/**
* Deletes a named mapping from the index
*/
public void deleteRoleMapping(DeleteRoleMappingRequest request, ActionListener<Boolean> listener) {
modifyMapping(request.getName(), this::innerDeleteMapping, request, listener);
}
private <Request, Result> void modifyMapping(String name, CheckedBiConsumer<Request, ActionListener<Result>, Exception> inner,
Request request, ActionListener<Result> listener) {
if (isTribeNode) {
listener.onFailure(new UnsupportedOperationException("role-mappings may not be modified using a tribe node"));
} else if (securityLifecycleService.isSecurityIndexWriteable() == false) {
listener.onFailure(new IllegalStateException("role-mappings cannot be modified until template and mappings are up to date"));
} else {
try {
inner.accept(request, ActionListener.wrap(r -> refreshRealms(listener, r), listener::onFailure));
} catch (Exception e) {
logger.error(new ParameterizedMessage("failed to modify role-mapping [{}]", name), e);
listener.onFailure(e);
}
}
}
private void innerPutMapping(PutRoleMappingRequest request, ActionListener<Boolean> listener) throws IOException {
final ExpressionRoleMapping mapping = request.getMapping();
client.prepareIndex(SecurityLifecycleService.SECURITY_INDEX_NAME, SECURITY_GENERIC_TYPE, getIdForName(mapping.getName()))
.setSource(mapping.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS, true))
.setRefreshPolicy(request.getRefreshPolicy())
.execute(new ActionListener<IndexResponse>() {
@Override
public void onResponse(IndexResponse indexResponse) {
boolean created = indexResponse.getResult() == CREATED;
listener.onResponse(created);
}
@Override
public void onFailure(Exception e) {
logger.error(new ParameterizedMessage("failed to put role-mapping [{}]", mapping.getName()), e);
listener.onFailure(e);
}
});
}
private void innerDeleteMapping(DeleteRoleMappingRequest request, ActionListener<Boolean> listener) throws IOException {
client.prepareDelete(SecurityLifecycleService.SECURITY_INDEX_NAME, SECURITY_GENERIC_TYPE, getIdForName(request.getName()))
.setRefreshPolicy(request.getRefreshPolicy())
.execute(new ActionListener<DeleteResponse>() {
@Override
public void onResponse(DeleteResponse deleteResponse) {
boolean deleted = deleteResponse.getResult() == DELETED;
listener.onResponse(deleted);
}
@Override
public void onFailure(Exception e) {
logger.error(new ParameterizedMessage("failed to delete role-mapping [{}]", request.getName()), e);
listener.onFailure(e);
}
});
}
/**
* Retrieves one or more mappings from the index.
* If <code>names</code> is <code>null</code> or {@link Set#isEmpty empty}, then this retrieves all mappings.
* Otherwise it retrieves the specified mappings by name.
*/
public void getRoleMappings(Set<String> names, ActionListener<List<ExpressionRoleMapping>> listener) {
if (names == null || names.isEmpty()) {
getMappings(listener);
} else {
getMappings(new ActionListener<List<ExpressionRoleMapping>>() {
@Override
public void onResponse(List<ExpressionRoleMapping> mappings) {
final List<ExpressionRoleMapping> filtered = mappings.stream()
.filter(m -> names.contains(m.getName()))
.collect(Collectors.toList());
listener.onResponse(filtered);
}
@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
});
}
}
private void getMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
if (securityLifecycleService.isSecurityIndexAvailable()) {
loadMappings(listener);
} else {
logger.info("The security index is not yet available - no role mappings can be loaded");
if (logger.isDebugEnabled()) {
logger.debug("Security Index [{}] [exists: {}] [available: {}] [writable: {}]",
SecurityLifecycleService.SECURITY_INDEX_NAME,
securityLifecycleService.isSecurityIndexExisting(),
securityLifecycleService.isSecurityIndexAvailable(),
securityLifecycleService.isSecurityIndexWriteable()
);
}
listener.onResponse(Collections.emptyList());
}
}
/**
* Provides usage statistics for this store.
* The resulting map contains the keys
* <ul>
* <li><code>size</code> - The total number of mappings stored in the index</li>
* <li><code>enabled</code> - The number of mappings that are
* {@link ExpressionRoleMapping#isEnabled() enabled}</li>
* </ul>
*/
public void usageStats(ActionListener<Map<String, Object>> listener) {
if (securityLifecycleService.isSecurityIndexExisting() == false) {
reportStats(listener, Collections.emptyList());
} else {
getMappings(ActionListener.wrap(mappings -> reportStats(listener, mappings), listener::onFailure));
}
}
private void reportStats(ActionListener<Map<String, Object>> listener, List<ExpressionRoleMapping> mappings) {
Map<String, Object> usageStats = new HashMap<>();
usageStats.put("size", mappings.size());
usageStats.put("enabled", mappings.stream().filter(ExpressionRoleMapping::isEnabled).count());
listener.onResponse(usageStats);
}
private <Result> void refreshRealms(ActionListener<Result> listener, Result result) {
String[] realmNames = this.realmsToRefresh.toArray(new String[realmsToRefresh.size()]);
new SecurityClient(this.client).prepareClearRealmCache().realms(realmNames).execute(ActionListener.wrap(
response -> {
logger.debug((Supplier<?>) () -> new ParameterizedMessage(
"Cleared cached in realms [{}] due to role mapping change", Arrays.toString(realmNames)));
listener.onResponse(result);
},
ex -> {
logger.warn("Failed to clear cache for realms [{}]", Arrays.toString(realmNames));
listener.onFailure(ex);
})
);
}
@Override
public void resolveRoles(UserData user, ActionListener<Set<String>> listener) {
getRoleMappings(null, ActionListener.wrap(
mappings -> {
final Map<String, Object> userDataMap = user.asMap();
Stream<ExpressionRoleMapping> stream = mappings.stream()
.filter(ExpressionRoleMapping::isEnabled)
.filter(m -> m.getExpression().match(userDataMap));
if (logger.isTraceEnabled()) {
stream = stream.map(m -> {
logger.trace("User [{}] matches role-mapping [{}] with roles [{}]", user.getUsername(), m.getName(),
m.getRoles());
return m;
});
}
final Set<String> roles = stream.flatMap(m -> m.getRoles().stream()).collect(Collectors.toSet());
logger.debug("Mapping user [{}] to roles [{}]", user, roles);
listener.onResponse(roles);
}, listener::onFailure
));
}
/**
* Indicates that the provided realm should have its cache cleared if this store is updated
* (that is, {@link #putRoleMapping(PutRoleMappingRequest, ActionListener)} or
* {@link #deleteRoleMapping(DeleteRoleMappingRequest, ActionListener)} are called).
* @see org.elasticsearch.xpack.security.action.realm.ClearRealmCacheAction
*/
@Override
public void refreshRealmOnChange(CachingUsernamePasswordRealm realm) {
realmsToRefresh.add(realm.name());
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
/**
* An expression that evaluates to <code>true</code> if-and-only-if all its children
* evaluate to <code>true</code>.
* An <em>all</em> expression with no children is always <code>true</code>.
*/
public final class AllExpression implements RoleMapperExpression {
static final String NAME = "all";
private final List<RoleMapperExpression> elements;
AllExpression(List<RoleMapperExpression> elements) {
assert elements != null;
this.elements = elements;
}
AllExpression(StreamInput in) throws IOException {
this(ExpressionParser.readExpressionList(in));
}
@Override
public void writeTo(StreamOutput out) throws IOException {
ExpressionParser.writeExpressionList(elements, out);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public boolean match(Map<String, Object> object) {
return elements.stream().allMatch(RoleMapperExpression.predicate(object));
}
public List<RoleMapperExpression> getElements() {
return Collections.unmodifiableList(elements);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final AllExpression that = (AllExpression) o;
return this.elements.equals(that.elements);
}
@Override
public int hashCode() {
return elements.hashCode();
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.startArray(ExpressionParser.Fields.ALL.getPreferredName());
for (RoleMapperExpression e : elements) {
e.toXContent(builder, params);
}
builder.endArray();
return builder.endObject();
}
}

View File

@ -0,0 +1,85 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
/**
* An expression that evaluates to <code>true</code> if at least one of its children
* evaluate to <code>true</code>.
* An <em>any</em> expression with no children is never <code>true</code>.
*/
public final class AnyExpression implements RoleMapperExpression {
static final String NAME = "any";
private final List<RoleMapperExpression> elements;
AnyExpression(List<RoleMapperExpression> elements) {
assert elements != null;
this.elements = elements;
}
AnyExpression(StreamInput in) throws IOException {
this(ExpressionParser.readExpressionList(in));
}
@Override
public void writeTo(StreamOutput out) throws IOException {
ExpressionParser.writeExpressionList(elements, out);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public boolean match(Map<String, Object> object) {
return elements.stream().anyMatch(RoleMapperExpression.predicate(object));
}
public List<RoleMapperExpression> getElements() {
return Collections.unmodifiableList(elements);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final AnyExpression that = (AnyExpression) o;
return this.elements.equals(that.elements);
}
@Override
public int hashCode() {
return elements.hashCode();
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.startArray(ExpressionParser.Fields.ANY.getPreferredName());
for (RoleMapperExpression e : elements) {
e.toXContent(builder, params);
}
builder.endArray();
return builder.endObject();
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.io.IOException;
import java.util.Map;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
/**
* A negating expression. That is, this expression evaluates to <code>true</code> if-and-only-if
* its delegate expression evaluate to <code>false</code>.
* Syntactically, <em>except</em> expressions are intended to be children of <em>all</em>
* expressions ({@link AllExpression}).
*/
public final class ExceptExpression implements RoleMapperExpression {
static final String NAME = "except";
private final RoleMapperExpression expression;
ExceptExpression(RoleMapperExpression expression) {
assert expression != null;
this.expression = expression;
}
ExceptExpression(StreamInput in) throws IOException {
this(ExpressionParser.readExpression(in));
}
@Override
public void writeTo(StreamOutput out) throws IOException {
ExpressionParser.writeExpression(expression, out);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public boolean match(Map<String, Object> object) {
return !expression.match(object);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final ExceptExpression that = (ExceptExpression) o;
return this.expression.equals(that.expression);
}
@Override
public int hashCode() {
return expression.hashCode();
}
RoleMapperExpression getInnerExpression() {
return expression;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(ExpressionParser.Fields.EXCEPT.getPreferredName());
expression.toXContent(builder, params);
return builder.endObject();
}
}

View File

@ -0,0 +1,203 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.xpack.watcher.support.xcontent.XContentSource;
/**
* Parses the JSON (XContent) based boolean expression DSL into a tree of {@link RoleMapperExpression} objects.
*/
public final class ExpressionParser {
public static final NamedWriteableRegistry.Entry[] NAMED_WRITEABLES = {
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AllExpression.NAME, AllExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AnyExpression.NAME, AnyExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, FieldExpression.NAME, FieldExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, ExceptExpression.NAME, ExceptExpression::new)
};
public static RoleMapperExpression readExpression(StreamInput in) throws IOException {
return in.readNamedWriteable(RoleMapperExpression.class);
}
public static void writeExpression(RoleMapperExpression expression, StreamOutput out) throws IOException {
out.writeNamedWriteable(expression);
}
static List<RoleMapperExpression> readExpressionList(StreamInput in) throws IOException {
return in.readNamedWriteableList(RoleMapperExpression.class);
}
static void writeExpressionList(List<RoleMapperExpression> list, StreamOutput out) throws IOException {
out.writeNamedWriteableList(list);
}
/**
* This function exists to be compatible with
* {@link org.elasticsearch.common.xcontent.ContextParser#parse(XContentParser, Object)}
*/
public static RoleMapperExpression parseObject(XContentParser parser, String id) throws IOException {
return new ExpressionParser().parse(id, parser);
}
/**
* @param name The name of the expression tree within its containing object. Used to provide
* descriptive error messages.
* @param content The XContent (typically JSON) DSL representation of the expression
*/
public RoleMapperExpression parse(String name, XContentSource content) throws IOException {
return parse(name, content.parser(NamedXContentRegistry.EMPTY));
}
/**
* @param name The name of the expression tree within its containing object. Used to provide
* descriptive error messages.
* @param parser A parser over the XContent (typically JSON) DSL representation of the
* expression
*/
public RoleMapperExpression parse(String name, XContentParser parser) throws IOException {
return parseRulesObject(name, parser, false);
}
private RoleMapperExpression parseRulesObject(String objectName, XContentParser parser,
boolean allowExcept) throws IOException {
// find the start of the DSL object
XContentParser.Token token;
if (parser.currentToken() == null) {
token = parser.nextToken();
} else {
token = parser.currentToken();
}
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("failed to parse rules expression. expected [{}] to be an object but found [{}] instead",
objectName, token);
}
final String fieldName = readFieldName(objectName, parser);
final RoleMapperExpression expr = parseExpression(parser, fieldName, allowExcept, objectName);
if (parser.nextToken() != XContentParser.Token.END_OBJECT) {
throw new ElasticsearchParseException("failed to parse rules expression. object [{}] contains multiple fields", objectName);
}
return expr;
}
private RoleMapperExpression parseExpression(XContentParser parser, String field, boolean allowExcept, String objectName)
throws IOException {
if (Fields.ANY.match(field)) {
return new AnyExpression(parseExpressionArray(Fields.ANY, parser, false));
} else if (Fields.ALL.match(field)) {
return new AllExpression(parseExpressionArray(Fields.ALL, parser, true));
} else if (Fields.FIELD.match(field)) {
return parseFieldExpression(parser);
} else if (Fields.EXCEPT.match(field)) {
if (allowExcept) {
return parseExceptExpression(parser);
} else {
throw new ElasticsearchParseException("failed to parse rules expression. field [{}] is not allowed within [{}]",
field, objectName);
}
} else {
throw new ElasticsearchParseException("failed to parse rules expression. field [{}] is not recognised in object [{}]",
field, objectName);
}
}
private RoleMapperExpression parseFieldExpression(XContentParser parser) throws IOException {
checkStartObject(parser);
final String fieldName = readFieldName(Fields.FIELD.getPreferredName(), parser);
final List<FieldExpression.FieldPredicate> values;
if (parser.nextToken() == XContentParser.Token.START_ARRAY) {
values = parseArray(Fields.FIELD, parser, this::parseFieldValue);
} else {
values = Collections.singletonList(parseFieldValue(parser));
}
if (parser.nextToken() != XContentParser.Token.END_OBJECT) {
throw new ElasticsearchParseException("failed to parse rules expression. object [{}] contains multiple fields",
Fields.FIELD.getPreferredName());
}
return new FieldExpression(fieldName, values);
}
private RoleMapperExpression parseExceptExpression(XContentParser parser) throws IOException {
checkStartObject(parser);
return new ExceptExpression(parseRulesObject(Fields.EXCEPT.getPreferredName(), parser, false));
}
private void checkStartObject(XContentParser parser) throws IOException {
final XContentParser.Token token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("failed to parse rules expression. expected an object but found [{}] instead", token);
}
}
private String readFieldName(String objectName, XContentParser parser) throws IOException {
if (parser.nextToken() != XContentParser.Token.FIELD_NAME) {
throw new ElasticsearchParseException("failed to parse rules expression. object [{}] does not contain any fields", objectName);
}
return parser.currentName();
}
private List<RoleMapperExpression> parseExpressionArray(ParseField field, XContentParser parser, boolean allowExcept)
throws IOException {
parser.nextToken(); // parseArray requires that the parser is positioned at the START_ARRAY token
return parseArray(field, parser, p -> parseRulesObject(field.getPreferredName(), p, allowExcept));
}
private <T> List<T> parseArray(ParseField field, XContentParser parser, CheckedFunction<XContentParser, T, IOException> elementParser)
throws IOException {
final XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.START_ARRAY) {
List<T> list = new ArrayList<>();
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
list.add(elementParser.apply(parser));
}
return list;
} else {
throw new ElasticsearchParseException("failed to parse rules expression. field [{}] requires an array", field);
}
}
private FieldExpression.FieldPredicate parseFieldValue(XContentParser parser) throws IOException {
switch (parser.currentToken()) {
case VALUE_STRING:
return FieldExpression.FieldPredicate.create(parser.text());
case VALUE_BOOLEAN:
return FieldExpression.FieldPredicate.create(parser.booleanValue());
case VALUE_NUMBER:
return FieldExpression.FieldPredicate.create(parser.longValue());
case VALUE_NULL:
return FieldExpression.FieldPredicate.create(null);
default:
throw new ElasticsearchParseException("failed to parse rules expression. expected a field value but found [{}] instead",
parser.currentToken());
}
}
public interface Fields {
ParseField ANY = new ParseField("any");
ParseField ALL = new ParseField("all");
ParseField EXCEPT = new ParseField("except");
ParseField FIELD = new ParseField("field");
}
}

View File

@ -0,0 +1,219 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import org.elasticsearch.common.Numbers;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.xpack.security.support.Automatons;
/**
* An expression that evaluates to <code>true</code> if a field (map element) matches
* the provided values. A <em>field</em> expression may have more than one provided value, in which
* case the expression is true if <em>any</em> of the values are matched.
*/
public final class FieldExpression implements RoleMapperExpression {
static final String NAME = "field";
private final String field;
private final List<FieldPredicate> values;
public FieldExpression(String field, List<FieldPredicate> values) {
if (field == null || field.isEmpty()) {
throw new IllegalArgumentException("null or empty field name (" + field + ")");
}
if (values == null || values.isEmpty()) {
throw new IllegalArgumentException("null or empty values (" + values + ")");
}
this.field = field;
this.values = values;
}
FieldExpression(StreamInput in) throws IOException {
this(in.readString(), in.readList(FieldPredicate::readFrom));
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(field);
out.writeList(values);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public boolean match(Map<String, Object> object) {
final Object fieldValue = object.get(field);
if (fieldValue instanceof Collection) {
return ((Collection) fieldValue).stream().anyMatch(this::matchValue);
} else {
return matchValue(fieldValue);
}
}
private boolean matchValue(Object fieldValue) {
return values.stream().anyMatch(predicate -> predicate.test(fieldValue));
}
public String getField() {
return field;
}
public List<Predicate<Object>> getValues() {
return Collections.unmodifiableList(values);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final FieldExpression that = (FieldExpression) o;
return this.field.equals(that.field) && this.values.equals(that.values);
}
@Override
public int hashCode() {
int result = field.hashCode();
result = 31 * result + values.hashCode();
return result;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.startObject(ExpressionParser.Fields.FIELD.getPreferredName());
if (this.values.size() == 1) {
builder.field(this.field);
values.get(0).toXContent(builder, params);
} else {
builder.startArray(this.field);
for (FieldPredicate fp : values) {
fp.toXContent(builder, params);
}
builder.endArray();
}
builder.endObject();
return builder.endObject();
}
/**
* A special predicate for matching values in a {@link FieldExpression}. This interface
* exists to support the serialisation ({@link ToXContent}, {@link Writeable}) of <em>field</em>
* expressions.
*/
public static class FieldPredicate implements Predicate<Object>, ToXContent, Writeable {
private final Object value;
private final Predicate<Object> predicate;
private FieldPredicate(Object value, Predicate<Object> predicate) {
this.value = value;
this.predicate = predicate;
}
@Override
public boolean test(Object o) {
return this.predicate.test(o);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params)
throws IOException {
return builder.value(value);
}
/**
* Create an appropriate predicate based on the type and value of the argument.
* The predicate is formed according to the following rules:
* <ul>
* <li>If <code>value</code> is <code>null</code>, then the predicate evaluates to
* <code>true</code> <em>if-and-only-if</em> the predicate-argument is
* <code>null</code></li>
* <li>If <code>value</code> is a {@link Boolean}, then the predicate
* evaluates to <code>true</code> <em>if-and-only-if</em> the predicate-argument is
* an {@link Boolean#equals(Object) equal} <code>Boolean</code></li>
* <li>If <code>value</code> is a {@link Number}, then the predicate
* evaluates to <code>true</code> <em>if-and-only-if</em> the predicate-argument is
* numerically equal to <code>value</code>. This class makes a best-effort to determine
* numeric equality across different implementations of <code>Number</code>, but the
* implementation can only be guaranteed for standard integral representations (
* <code>Long</code>, <code>Integer</code>, etc)</li>
* <li>If <code>value</code> is a {@link String}, then it is treated as a
* {@link org.apache.lucene.util.automaton.Automaton Lucene automaton} pattern with
* {@link Automatons#predicate(String...) corresponding predicate}.
* </li>
* </ul>
*/
public static FieldPredicate create(Object value) {
Predicate<Object> predicate = buildPredicate(value);
return new FieldPredicate(value, predicate);
}
public static FieldPredicate readFrom(StreamInput in) throws IOException {
return create(in.readGenericValue());
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeGenericValue(value);
}
private static Predicate<Object> buildPredicate(Object object) {
if (object == null) {
return Objects::isNull;
}
if (object instanceof Boolean) {
return object::equals;
}
if (object instanceof Number) {
return (other) -> numberEquals((Number) object, other);
}
if (object instanceof String) {
final String str = (String) object;
if (str.isEmpty()) {
return obj -> String.valueOf(obj).isEmpty();
} else {
final Predicate<String> predicate = Automatons.predicate(str);
return obj -> predicate.test(String.valueOf(obj));
}
}
throw new IllegalArgumentException("Unsupported value type " + object.getClass());
}
private static boolean numberEquals(Number left, Object other) {
if (left.equals(other)) {
return true;
}
if ((other instanceof Number) == false) {
return false;
}
Number right = (Number) other;
if (left instanceof Double || left instanceof Float
|| right instanceof Double || right instanceof Float) {
return Double.compare(left.doubleValue(), right.doubleValue()) == 0;
}
return Numbers.toLongExact(left) == Numbers.toLongExact(right);
}
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.util.Map;
import java.util.function.Predicate;
import org.elasticsearch.common.io.stream.NamedWriteable;
import org.elasticsearch.common.xcontent.ToXContentObject;
/**
* Implementations of this interface represent an expression over a simple object that resolves to
* a boolean value. The "simple object" is implemented as a (flattened) {@link Map}.
*/
public interface RoleMapperExpression extends ToXContentObject, NamedWriteable {
/**
* Determines whether this expression matches against the provided object.
*/
boolean match(Map<String, Object> object);
/**
* Adapt this expression to a standard {@link Predicate}
*/
default Predicate<Map<String, Object>> asPredicate() {
return this::match;
}
/**
* Creates an <em>inverted</em> predicate that can test whether an expression matches
* a fixed object. Its purpose is for cases where there is a {@link java.util.stream.Stream} of
* expressions, that need to be filtered against a single map.
*/
static Predicate<RoleMapperExpression> predicate(Map<String, Object> map) {
return expr -> expr.match(map);
}
}

View File

@ -35,6 +35,7 @@ import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.license.LicenseUtils;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.action.role.ClearRolesCacheRequest;
@ -87,7 +88,7 @@ public class NativeRolesStore extends AbstractComponent {
SecurityLifecycleService securityLifecycleService) {
super(settings);
this.client = client;
this.isTribeNode = settings.getGroups("tribe", true).isEmpty() == false;
this.isTribeNode = XPackPlugin.isTribeNode(settings);
this.securityClient = new SecurityClient(client);
this.licenseState = licenseState;
this.securityLifecycleService = securityLifecycleService;

View File

@ -30,6 +30,14 @@ import org.elasticsearch.xpack.security.action.role.PutRoleAction;
import org.elasticsearch.xpack.security.action.role.PutRoleRequest;
import org.elasticsearch.xpack.security.action.role.PutRoleRequestBuilder;
import org.elasticsearch.xpack.security.action.role.PutRoleResponse;
import org.elasticsearch.xpack.security.action.rolemapping.DeleteRoleMappingAction;
import org.elasticsearch.xpack.security.action.rolemapping.DeleteRoleMappingRequestBuilder;
import org.elasticsearch.xpack.security.action.rolemapping.GetRoleMappingsAction;
import org.elasticsearch.xpack.security.action.rolemapping.GetRoleMappingsRequest;
import org.elasticsearch.xpack.security.action.rolemapping.GetRoleMappingsRequestBuilder;
import org.elasticsearch.xpack.security.action.rolemapping.GetRoleMappingsResponse;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingAction;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingRequestBuilder;
import org.elasticsearch.xpack.security.action.token.CreateTokenAction;
import org.elasticsearch.xpack.security.action.token.CreateTokenRequest;
import org.elasticsearch.xpack.security.action.token.CreateTokenRequestBuilder;
@ -239,6 +247,28 @@ public class SecurityClient {
client.execute(PutRoleAction.INSTANCE, request, listener);
}
/** Role Mappings */
public GetRoleMappingsRequestBuilder prepareGetRoleMappings(String... names) {
return new GetRoleMappingsRequestBuilder(client, GetRoleMappingsAction.INSTANCE)
.names(names);
}
public void getRoleMappings(GetRoleMappingsRequest request,
ActionListener<GetRoleMappingsResponse> listener) {
client.execute(GetRoleMappingsAction.INSTANCE, request, listener);
}
public PutRoleMappingRequestBuilder preparePutRoleMapping(
String name, BytesReference content, XContentType xContentType) throws IOException {
return new PutRoleMappingRequestBuilder(client, PutRoleMappingAction.INSTANCE).source(name, content, xContentType);
}
public DeleteRoleMappingRequestBuilder prepareDeleteRoleMapping(String name) {
return new DeleteRoleMappingRequestBuilder(client, DeleteRoleMappingAction.INSTANCE)
.name(name);
}
public CreateTokenRequestBuilder prepareCreateToken() {
return new CreateTokenRequestBuilder(client);
}

View File

@ -0,0 +1,51 @@
/*
* 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.rest.action.rolemapping;
import java.io.IOException;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.security.action.rolemapping.DeleteRoleMappingResponse;
import org.elasticsearch.xpack.security.client.SecurityClient;
import static org.elasticsearch.rest.RestRequest.Method.DELETE;
/**
* Rest endpoint to delete a role-mapping from the {@link org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore}
*/
public class RestDeleteRoleMappingAction extends BaseRestHandler {
public RestDeleteRoleMappingAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(DELETE, "/_xpack/security/role_mapping/{name}", this);
}
@Override
public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client)
throws IOException {
final String name = request.param("name");
final String refresh = request.param("refresh");
return channel -> new SecurityClient(client).prepareDeleteRoleMapping(name)
.setRefreshPolicy(refresh)
.execute(new RestBuilderListener<DeleteRoleMappingResponse>(channel) {
@Override
public RestResponse buildResponse(DeleteRoleMappingResponse response, XContentBuilder builder) throws Exception {
return new BytesRestResponse(response.isFound() ? RestStatus.OK : RestStatus.NOT_FOUND,
builder.startObject().field("found", response.isFound()).endObject());
}
});
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.rest.action.rolemapping;
import java.io.IOException;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.security.action.rolemapping.GetRoleMappingsResponse;
import org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.security.client.SecurityClient;
import static org.elasticsearch.rest.RestRequest.Method.GET;
/**
* Rest endpoint to retrieve a role-mapping from the {@link org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore}
*/
public class RestGetRoleMappingsAction extends BaseRestHandler {
public RestGetRoleMappingsAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(GET, "/_xpack/security/role_mapping/", this);
controller.registerHandler(GET, "/_xpack/security/role_mapping/{name}", this);
}
@Override
public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client)
throws IOException {
final String[] names = request.paramAsStringArrayOrEmptyIfAll("name");
return channel -> new SecurityClient(client).prepareGetRoleMappings(names)
.execute(new RestBuilderListener<GetRoleMappingsResponse>(channel) {
@Override
public RestResponse buildResponse(GetRoleMappingsResponse response, XContentBuilder builder) throws Exception {
builder.startObject();
for (ExpressionRoleMapping mapping : response.mappings()) {
builder.field(mapping.getName(), mapping);
}
builder.endObject();
// if the request specified mapping names, but nothing was found then return a 404 result
if (names.length != 0 && response.mappings().length == 0) {
return new BytesRestResponse(RestStatus.NOT_FOUND, builder);
} else {
return new BytesRestResponse(RestStatus.OK, builder);
}
}
});
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.rest.action.rolemapping;
import java.io.IOException;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingRequestBuilder;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingResponse;
import org.elasticsearch.xpack.security.client.SecurityClient;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;
/**
* Rest endpoint to add a role-mapping to the native store
*
* @see org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore
*/
public class RestPutRoleMappingAction extends BaseRestHandler {
public RestPutRoleMappingAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(POST, "/_xpack/security/role_mapping/{name}", this);
controller.registerHandler(PUT, "/_xpack/security/role_mapping/{name}", this);
}
@Override
public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client)
throws IOException {
final String name = request.param("name");
PutRoleMappingRequestBuilder requestBuilder = new SecurityClient(client)
.preparePutRoleMapping(name, request.content(), request.getXContentType())
.setRefreshPolicy(request.param("refresh"));
return channel -> requestBuilder.execute(
new RestBuilderListener<PutRoleMappingResponse>(channel) {
@Override
public RestResponse buildResponse(PutRoleMappingResponse response, XContentBuilder builder) throws Exception {
return new BytesRestResponse(RestStatus.OK, builder.startObject().field("role_mapping", response).endObject());
}
});
}
}

View File

@ -268,7 +268,8 @@ public class IndexLifecycleManager extends AbstractComponent {
Map<String, Object> meta =
(Map<String, Object>) mappingMetaData.sourceAsMap().get("_meta");
if (meta == null) {
throw new IllegalStateException("Cannot read security-version string");
logger.info("Missing _meta field in mapping [{}] of index [{}]", mappingMetaData.type(), indexName);
throw new IllegalStateException("Cannot read security-version string in index " + indexName);
}
return Version.fromString((String) meta.get(SECURITY_VERSION_STRING));
} catch (IOException e) {
@ -306,6 +307,11 @@ public class IndexLifecycleManager extends AbstractComponent {
indexName, previousVersion),
e);
}
@Override
public String toString() {
return getClass() + "{" + indexName + " migrator}";
}
});
return true;
} else {
@ -383,6 +389,11 @@ public class IndexLifecycleManager extends AbstractComponent {
"failed to update mapping for type [{}] on index [{}]",
type, indexName), e);
}
@Override
public String toString() {
return getClass() + "{" + indexName + " PutMapping}";
}
});
}
@ -417,6 +428,11 @@ public class IndexLifecycleManager extends AbstractComponent {
logger.warn(new ParameterizedMessage(
"failed to put template [{}]", templateName), e);
}
@Override
public String toString() {
return getClass() + "{" + indexName + " PutTemplate}";
}
});
}
}

View File

@ -138,6 +138,20 @@
"expiration_time": {
"type": "date",
"format": "date_time"
},
"roles": {
"type": "keyword"
},
"rules": {
"type" : "object",
"dynamic" : true
},
"enabled": {
"type": "boolean"
},
"metadata" : {
"type" : "object",
"dynamic" : true
}
}
}

View File

@ -7,21 +7,39 @@ package org.elasticsearch.integration.ldap;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.ListenableActionFuture;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.SecurityIntegTestCase;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingRequestBuilder;
import org.elasticsearch.xpack.security.action.rolemapping.PutRoleMappingResponse;
import org.elasticsearch.xpack.security.authc.ldap.LdapRealm;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.security.client.SecurityClient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope.ONE_LEVEL;
@ -34,34 +52,80 @@ import static org.hamcrest.Matchers.equalTo;
* This test assumes all subclass tests will be of type SUITE. It picks a random realm configuration for the tests, and
* writes a group to role mapping file for each node.
*/
public abstract class AbstractAdLdapRealmTestCase extends SecurityIntegTestCase {
public abstract class AbstractAdLdapRealmTestCase extends SecurityIntegTestCase {
public static final String XPACK_SECURITY_AUTHC_REALMS_EXTERNAL = "xpack.security.authc.realms.external";
public static final String PASSWORD = "NickFuryHeartsES";
public static final String ASGARDIAN_INDEX = "gods";
public static final String PHILANTHROPISTS_INDEX = "philanthropists";
public static final String SECURITY_INDEX = "security";
private static final String AD_ROLE_MAPPING =
"SHIELD: [ \"CN=SHIELD,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n" +
"Avengers: [ \"CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n" +
"Gods: [ \"CN=Gods,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n" +
"Philanthropists: [ \"CN=Philanthropists,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n";
private static final String OLDAP_ROLE_MAPPING =
"SHIELD: [ \"cn=SHIELD,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n" +
"Avengers: [ \"cn=Avengers,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n" +
"Gods: [ \"cn=Gods,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n" +
"Philanthropists: [ \"cn=Philanthropists,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n";
private static final RoleMappingEntry[] AD_ROLE_MAPPING = new RoleMappingEntry[] {
new RoleMappingEntry(
"SHIELD: [ \"CN=SHIELD,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ]",
"{ \"roles\":[\"SHIELD\"], \"enabled\":true, \"rules\":" +
"{\"field\": {\"groups\": \"CN=SHIELD,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"} } }"
),
new RoleMappingEntry(
"Avengers: [ \"CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ]",
"{ \"roles\":[\"Avengers\"], \"enabled\":true, \"rules\":" +
"{ \"field\": { \"groups\" : \"CN=Avengers,CN=Users,*\" } } }"
),
new RoleMappingEntry(
"Gods: [ \"CN=Gods,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ]",
"{ \"roles\":[\"Gods\"], \"enabled\":true, \"rules\":{\"any\": [" +
" { \"field\":{ \"groups\": \"CN=Gods,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" } }," +
" { \"field\":{ \"groups\": \"CN=Deities,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" } } " +
"] } }"
),
new RoleMappingEntry(
"Philanthropists: [ \"CN=Philanthropists,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ]",
"{ \"roles\":[\"Philanthropists\"], \"enabled\":true, \"rules\": { \"all\": [" +
" { \"field\": { \"groups\" : \"CN=Philanthropists,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" } }," +
" { \"field\": { \"realm.name\" : \"external\" } } " +
"] } }"
)
};
private static final RoleMappingEntry[] OLDAP_ROLE_MAPPING = new RoleMappingEntry[] {
new RoleMappingEntry(
"SHIELD: [ \"cn=SHIELD,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ]",
"{ \"roles\": [\"SHIELD\"], \"enabled\":true, \"rules\":" +
" {\"field\": {\"groups\": \"cn=SHIELD,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" } } }"
),
new RoleMappingEntry(
"Avengers: [ \"cn=Avengers,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ]",
"{ \"roles\": [\"Avengers\"], \"enabled\":true, \"rules\":" +
" {\"field\": {\"groups\": \"cn=Avengers,ou=people,*\" } } }"
),
new RoleMappingEntry(
"Gods: [ \"cn=Gods,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ]",
"{ \"roles\" : [ \"Gods\" ], \"enabled\":true, \"rules\" : { \"any\": [" +
" {\"field\": {\"groups\": \"cn=Gods,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" } }," +
" {\"field\": {\"groups\": \"cn=Deities,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" } } " +
"] } }"
),
new RoleMappingEntry(
"Philanthropists: [ \"cn=Philanthropists,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ]",
"{ \"roles\" : [ \"Philanthropists\" ], \"enabled\":true, \"rules\" : { \"all\": [" +
" { \"field\": { \"groups\" : \"cn=Philanthropists,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" } }, " +
" { \"field\": { \"realm.name\" : \"external\" } } " +
"] } }"
)
};
protected static final String TESTNODE_KEYSTORE = "/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks";
protected static RealmConfig realmConfig;
protected static List<RoleMappingEntry> roleMappings;
protected static boolean useGlobalSSL;
@BeforeClass
public static void setupRealm() {
realmConfig = randomFrom(RealmConfig.values());
roleMappings = realmConfig.selectRoleMappings(ESTestCase::randomBoolean);
useGlobalSSL = randomBoolean();
ESLoggerFactory.getLogger("test").info("running test with realm configuration [{}], with direct group to role mapping [{}]. " +
"Settings [{}]", realmConfig, realmConfig.mapGroupsAsRoles, realmConfig.settings.getAsMap());
"Settings [{}]", realmConfig, realmConfig.mapGroupsAsRoles, realmConfig.settings.getAsMap());
}
@AfterClass
@ -86,13 +150,46 @@ public abstract class AbstractAdLdapRealmTestCase extends SecurityIntegTestCase
protected Settings buildRealmSettings(RealmConfig realm, Path store) {
Settings.Builder builder = Settings.builder();
Path nodeFiles = createTempDir();
builder.put(realm.buildSettings(store, "testnode"))
.put(XPACK_SECURITY_AUTHC_REALMS_EXTERNAL + ".files.role_mapping", writeFile(nodeFiles, "role_mapping.yml",
configRoleMappings(realm)));
builder.put(realm.buildSettings(store, "testnode"));
configureRoleMappings(builder);
return builder.build();
}
@Before
public void setupRoleMappings() throws Exception {
assertSecurityIndexActive();
List<String> content = getRoleMappingContent(RoleMappingEntry::getNativeContent);
if (content.isEmpty()) {
return;
}
SecurityClient securityClient = securityClient();
Map<String, ListenableActionFuture<PutRoleMappingResponse>> futures
= new LinkedHashMap<>(content.size());
for (int i = 0; i < content.size(); i++) {
final String name = "external_" + i;
final PutRoleMappingRequestBuilder builder = securityClient.preparePutRoleMapping(
name, new BytesArray(content.get(i)), XContentType.JSON);
futures.put(name, builder.execute());
}
for (String mappingName : futures.keySet()) {
final PutRoleMappingResponse response = futures.get(mappingName).get();
logger.info("Created native role-mapping {} : {}", mappingName, response.isCreated());
}
}
@After
public void cleanupSecurityIndex() throws Exception {
super.deleteSecurityIndex();
}
private List<String> getRoleMappingContent(Function<RoleMappingEntry, String> contentFunction) {
return roleMappings.stream()
.map(contentFunction)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Override
protected Settings transportClientSettings() {
if (useGlobalSSL) {
@ -105,13 +202,17 @@ public abstract class AbstractAdLdapRealmTestCase extends SecurityIntegTestCase
return super.transportClientSettings();
}
}
@Override
protected boolean useGeneratedSSLConfig() {
return useGlobalSSL == false;
}
protected String configRoleMappings(RealmConfig realm) {
return realm.configRoleMappings();
protected final void configureRoleMappings(Settings.Builder builder) {
String content = getRoleMappingContent(RoleMappingEntry::getFileContent).stream().collect(Collectors.joining("\n"));
Path nodeFiles = createTempDir();
String file = writeFile(nodeFiles, "role_mapping.yml", content);
builder.put(XPACK_SECURITY_AUTHC_REALMS_EXTERNAL + ".files.role_mapping", file);
}
@Override
@ -189,6 +290,41 @@ public abstract class AbstractAdLdapRealmTestCase extends SecurityIntegTestCase
.put("xpack.ssl.truststore.password", password).build();
}
static class RoleMappingEntry {
@Nullable
public final String fileContent;
@Nullable
public final String nativeContent;
RoleMappingEntry(@Nullable String fileContent, @Nullable String nativeContent) {
this.fileContent = fileContent;
this.nativeContent = nativeContent;
}
String getFileContent() {
return fileContent;
}
String getNativeContent() {
return nativeContent;
}
RoleMappingEntry pickEntry(Supplier<Boolean> shouldPickFileContent) {
if (nativeContent == null) {
return new RoleMappingEntry(fileContent, null);
}
if (fileContent == null) {
return new RoleMappingEntry(null, nativeContent);
}
if (shouldPickFileContent.get()) {
return new RoleMappingEntry(fileContent, null);
} else {
return new RoleMappingEntry(null, nativeContent);
}
}
}
/**
* Represents multiple possible configurations for active directory and ldap
*/
@ -245,10 +381,10 @@ public abstract class AbstractAdLdapRealmTestCase extends SecurityIntegTestCase
final boolean mapGroupsAsRoles;
final boolean loginWithCommonName;
private final String roleMappings;
private final RoleMappingEntry[] roleMappings;
final Settings settings;
RealmConfig(boolean loginWithCommonName, String roleMappings, Settings settings) {
RealmConfig(boolean loginWithCommonName, RoleMappingEntry[] roleMappings, Settings settings) {
this.settings = settings;
this.loginWithCommonName = loginWithCommonName;
this.roleMappings = roleMappings;
@ -273,9 +409,15 @@ public abstract class AbstractAdLdapRealmTestCase extends SecurityIntegTestCase
return builder.build();
}
//if mapGroupsAsRoles is turned on we don't write anything to the rolemapping file
public String configRoleMappings() {
return mapGroupsAsRoles ? "" : roleMappings;
public List<RoleMappingEntry> selectRoleMappings(Supplier<Boolean> shouldPickFileContent) {
// if mapGroupsAsRoles is turned on we use empty role mapping
if (mapGroupsAsRoles) {
return Collections.emptyList();
} else {
return Arrays.stream(this.roleMappings)
.map(e -> e.pickEntry(shouldPickFileContent))
.collect(Collectors.toList());
}
}
}
}

View File

@ -5,16 +5,33 @@
*/
package org.elasticsearch.integration.ldap;
import org.elasticsearch.test.junit.annotations.Network;
import java.io.IOException;
import java.util.ArrayList;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.BeforeClass;
/**
* This tests the mapping of multiple groups to a role
* This tests the mapping of multiple groups to a role in a file based role-mapping
*/
@Network
public class MultiGroupMappingTests extends AbstractAdLdapRealmTestCase {
@BeforeClass
public static void setRoleMappingType() {
final String extraContent = "MarvelCharacters:\n" +
" - \"CN=SHIELD,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Gods,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Philanthropists,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"cn=SHIELD,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Avengers,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Gods,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Philanthropists,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n";
roleMappings = new ArrayList<>(roleMappings);
roleMappings.add(new RoleMappingEntry(extraContent, null));
}
@Override
protected String configRoles() {
return super.configRoles() +
@ -26,19 +43,6 @@ public class MultiGroupMappingTests extends AbstractAdLdapRealmTestCase {
" privileges: [ all ]\n";
}
@Override
protected String configRoleMappings(RealmConfig realm) {
return "MarvelCharacters: \n" +
" - \"CN=SHIELD,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Gods,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Philanthropists,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"cn=SHIELD,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Avengers,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Gods,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Philanthropists,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"";
}
public void testGroupMapping() throws IOException {
String asgardian = "odin";
String securityPhilanthropist = realmConfig.loginWithCommonName ? "Bruce Banner" : "hulk";

View File

@ -7,8 +7,9 @@ package org.elasticsearch.integration.ldap;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.settings.Settings;
@ -27,13 +28,14 @@ public class MultipleAdRealmTests extends AbstractAdLdapRealmTestCase {
@BeforeClass
public static void setupSecondaryRealm() {
// It's easier to test 2 realms when using file based role mapping, and for the purposes of
// this test, there's no need to test native mappings.
AbstractAdLdapRealmTestCase.roleMappings = realmConfig.selectRoleMappings(() -> true);
// Pick a secondary realm that has the inverse value for 'loginWithCommonName' compare with the primary realm
final List<RealmConfig> configs = new ArrayList<>();
for (RealmConfig config : RealmConfig.values()) {
if (config.loginWithCommonName != AbstractAdLdapRealmTestCase.realmConfig.loginWithCommonName) {
configs.add(config);
}
}
final List<RealmConfig> configs = Arrays.stream(RealmConfig.values())
.filter(config -> config.loginWithCommonName != AbstractAdLdapRealmTestCase.realmConfig.loginWithCommonName)
.collect(Collectors.toList());
secondaryRealmConfig = randomFrom(configs);
ESLoggerFactory.getLogger("test")
.info("running test with secondary realm configuration [{}], with direct group to role mapping [{}]. Settings [{}]",

View File

@ -23,12 +23,7 @@ public abstract class NativeRealmIntegTestCase extends SecurityIntegTestCase {
@After
public void stopESNativeStores() throws Exception {
try {
// this is a hack to clean up the .security index since only the XPack user can delete it
internalClient().admin().indices().prepareDelete(SecurityLifecycleService.SECURITY_INDEX_NAME).get();
} catch (IndexNotFoundException e) {
// ignore it since not all tests create this index...
}
deleteSecurityIndex();
if (getCurrentClusterScope() == Scope.SUITE) {
// Clear the realm cache for all realms since we use a SUITE scoped cluster

View File

@ -21,6 +21,7 @@ import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.xpack.XPackClient;
import org.elasticsearch.xpack.XPackPlugin;
@ -462,4 +463,13 @@ public abstract class SecurityIntegTestCase extends ESIntegTestCase {
});
}
}
protected void deleteSecurityIndex() {
try {
// this is a hack to clean up the .security index since only the XPack user can delete it
internalClient().admin().indices().prepareDelete(SecurityLifecycleService.SECURITY_INDEX_NAME).get();
} catch (IndexNotFoundException e) {
// ignore it since not all tests create this index...
}
}
}

View File

@ -21,6 +21,7 @@ import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.security.audit.AuditTrailService;
import org.elasticsearch.xpack.security.authc.Realms;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.transport.filter.IPFilter;
@ -55,6 +56,7 @@ public class SecurityFeatureSetTests extends ESTestCase {
private Realms realms;
private IPFilter ipFilter;
private CompositeRolesStore rolesStore;
private NativeRoleMappingStore roleMappingStore;
private AuditTrailService auditTrail;
private CryptoService cryptoService;
@ -66,11 +68,12 @@ public class SecurityFeatureSetTests extends ESTestCase {
realms = mock(Realms.class);
ipFilter = mock(IPFilter.class);
rolesStore = mock(CompositeRolesStore.class);
roleMappingStore = mock(NativeRoleMappingStore.class);
}
public void testAvailable() throws Exception {
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms, rolesStore,
ipFilter, environment);
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms,
rolesStore, roleMappingStore, ipFilter, environment);
boolean available = randomBoolean();
when(licenseState.isAuthAllowed()).thenReturn(available);
assertThat(featureSet.available(), is(available));
@ -82,14 +85,14 @@ public class SecurityFeatureSetTests extends ESTestCase {
.put(this.settings)
.put("xpack.security.enabled", enabled)
.build();
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms, rolesStore,
ipFilter, environment);
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms,
rolesStore, roleMappingStore, ipFilter, environment);
assertThat(featureSet.enabled(), is(enabled));
}
public void testEnabledDefault() throws Exception {
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms, rolesStore,
ipFilter, environment);
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms,
rolesStore, roleMappingStore, ipFilter, environment);
assertThat(featureSet.enabled(), is(true));
}
@ -100,8 +103,8 @@ public class SecurityFeatureSetTests extends ESTestCase {
Files.createDirectories(path.getParent());
Files.write(path, new byte[0]);
}
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms, rolesStore,
ipFilter, environment);
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings, licenseState, realms,
rolesStore, roleMappingStore, ipFilter, environment);
assertThat(featureSet.systemKeyUsage(), hasEntry("enabled", enabled));
}
@ -119,7 +122,11 @@ public class SecurityFeatureSetTests extends ESTestCase {
settings.put("xpack.security.http.ssl.enabled", httpSSLEnabled);
final boolean auditingEnabled = randomBoolean();
settings.put(XPackSettings.AUDIT_ENABLED.getKey(), auditingEnabled);
final String[] auditOutputs = randomFrom(new String[] {"logfile"}, new String[] {"index"}, new String[] {"logfile", "index"});
final String[] auditOutputs = randomFrom(
new String[] { "logfile" },
new String[] { "index" },
new String[] { "logfile", "index" }
);
settings.putArray(Security.AUDIT_OUTPUTS_SETTING.getKey(), auditOutputs);
final boolean httpIpFilterEnabled = randomBoolean();
final boolean transportIPFilterEnabled = randomBoolean();
@ -140,6 +147,21 @@ public class SecurityFeatureSetTests extends ESTestCase {
}
return Void.TYPE;
}).when(rolesStore).usageStats(any(ActionListener.class));
final boolean roleMappingStoreEnabled = randomBoolean();
doAnswer(invocationOnMock -> {
ActionListener<Map<String, Object>> listener = (ActionListener) invocationOnMock.getArguments()[0];
if (roleMappingStoreEnabled) {
final Map<String, Object> map = new HashMap<>();
map.put("size", 12L);
map.put("enabled", 10L);
listener.onResponse(map);
} else {
listener.onResponse(Collections.emptyMap());
}
return Void.TYPE;
}).when(roleMappingStore).usageStats(any(ActionListener.class));
final boolean useSystemKey = randomBoolean();
if (useSystemKey) {
Path path = CryptoService.resolveSystemKey(environment);
@ -162,7 +184,8 @@ public class SecurityFeatureSetTests extends ESTestCase {
settings.put(AnonymousUser.ROLES_SETTING.getKey(), "foo");
}
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings.build(), licenseState, realms, rolesStore, ipFilter, environment);
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings.build(), licenseState,
realms, rolesStore, roleMappingStore, ipFilter, environment);
PlainActionFuture<XPackFeatureSet.Usage> future = new PlainActionFuture<>();
featureSet.usage(future);
XPackFeatureSet.Usage securityUsage = future.get();
@ -209,6 +232,14 @@ public class SecurityFeatureSetTests extends ESTestCase {
assertThat(((Map) source.getValue("roles")).isEmpty(), is(true));
}
// role-mapping
if (roleMappingStoreEnabled) {
assertThat(source.getValue("role_mapping.native.size"), is(12));
assertThat(source.getValue("role_mapping.native.enabled"), is(10));
} else {
assertThat(((Map) source.getValue("role_mapping")).isEmpty(), is(true));
}
// system key
assertThat(source.getValue("system_key.enabled"), is(useSystemKey));

View File

@ -5,6 +5,13 @@
*/
package org.elasticsearch.xpack.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.action.Action;
@ -44,16 +51,12 @@ import org.junit.After;
import org.junit.Before;
import org.mockito.Mockito;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import static org.elasticsearch.xpack.security.SecurityLifecycleService.SECURITY_INDEX_NAME;
import static org.elasticsearch.xpack.security.SecurityLifecycleService.SECURITY_TEMPLATE_NAME;
import static org.elasticsearch.xpack.security.SecurityLifecycleService.securityIndexMappingAndTemplateSufficientToRead;
import static org.elasticsearch.xpack.security.SecurityLifecycleService.securityIndexMappingAndTemplateUpToDate;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Matchers.any;
@ -118,8 +121,9 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
}
public void testIndexTemplateIsIdentifiedAsUpToDate() throws IOException {
String templateString = "/" + SECURITY_TEMPLATE_NAME + ".json";
ClusterState.Builder clusterStateBuilder = createClusterStateWithTemplate(templateString);
ClusterState.Builder clusterStateBuilder = createClusterStateWithTemplate(
"/" + SECURITY_TEMPLATE_NAME + ".json"
);
securityLifecycleService.clusterChanged(new ClusterChangedEvent("test-event",
clusterStateBuilder.build(), EMPTY_CLUSTER_STATE));
assertThat(securityLifecycleService.securityIndex().isTemplateUpToDate(), equalTo(true));
@ -149,10 +153,12 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
private void checkTemplateUpdateWorkCorrectly(ClusterState.Builder clusterStateBuilder)
throws IOException {
final int numberOfSecurityIndices = 1; // .security
securityLifecycleService.clusterChanged(new ClusterChangedEvent("test-event",
clusterStateBuilder.build(), EMPTY_CLUSTER_STATE));
assertThat(securityLifecycleService.securityIndex().isTemplateUpToDate(), equalTo(false));
assertThat(listeners.size(), equalTo(1));
assertThat(listeners.size(), equalTo(numberOfSecurityIndices));
assertTrue(securityLifecycleService.securityIndex().isTemplateCreationPending());
// if we do it again this should not send an update
@ -199,7 +205,7 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
ClusterState.Builder clusterStateBuilder = new ClusterState.Builder(state());
// add the correct mapping
String mappingString = "/" + SECURITY_TEMPLATE_NAME + ".json";
IndexMetaData.Builder indexMeta = createIndexMetadata(mappingString);
IndexMetaData.Builder indexMeta = createIndexMetadata(SECURITY_INDEX_NAME, mappingString);
MetaData.Builder builder = new MetaData.Builder(clusterStateBuilder.build().getMetaData());
builder.put(indexMeta);
clusterStateBuilder.metaData(builder);
@ -223,7 +229,8 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
}
private void checkMappingUpdateWorkCorrectly(ClusterState.Builder clusterStateBuilder, Version expectedOldVersion) {
final int expectedNumberOfListeners = 4; // we have four types in the mapping
final int numberOfSecurityTypes = 4; // we have 4 types in the security mapping
final int totalNumberOfTypes = numberOfSecurityTypes ;
AtomicReference<Version> migratorVersionRef = new AtomicReference<>(null);
AtomicReference<ActionListener<Boolean>> migratorListenerRef = new AtomicReference<>(null);
@ -241,51 +248,54 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
assertThat(migratorVersionRef.get(), equalTo(expectedOldVersion));
assertThat(migratorListenerRef.get(), notNullValue());
assertThat(listeners.size(), equalTo(0)); // migrator has not responded yet
// security migrator has not responded yet
assertThat(this.listeners.size(), equalTo(0));
assertThat(securityIndex.isMappingUpdatePending(), equalTo(false));
assertThat(securityIndex.getMigrationState(), equalTo(UpgradeState.IN_PROGRESS));
migratorListenerRef.get().onResponse(true);
assertThat(listeners.size(), equalTo(expectedNumberOfListeners));
assertThat(this.listeners, iterableWithSize(totalNumberOfTypes));
assertThat(securityIndex.isMappingUpdatePending(), equalTo(true));
assertThat(securityIndex.getMigrationState(), equalTo(UpgradeState.COMPLETE));
// if we do it again this should not send an update
ActionListener listener = listeners.get(0);
listeners.clear();
List<ActionListener> cloneListeners = new ArrayList<>(this.listeners);
this.listeners.clear();
securityLifecycleService.clusterChanged(new ClusterChangedEvent("test-event",
clusterStateBuilder.build(), EMPTY_CLUSTER_STATE));
assertThat(listeners.size(), equalTo(0));
assertThat(this.listeners.size(), equalTo(0));
assertThat(securityIndex.isMappingUpdatePending(), equalTo(true));
// if we now simulate an error...
listener.onFailure(new Exception("Testing failure handling"));
cloneListeners.forEach(l -> l.onFailure(new Exception("Testing failure handling")));
assertThat(securityIndex.isMappingUpdatePending(), equalTo(false));
// ... we should be able to send a new update
securityLifecycleService.clusterChanged(new ClusterChangedEvent("test-event",
clusterStateBuilder.build(), EMPTY_CLUSTER_STATE));
assertThat(listeners.size(), equalTo(expectedNumberOfListeners));
assertThat(this.listeners.size(), equalTo(totalNumberOfTypes));
assertThat(securityIndex.isMappingUpdatePending(), equalTo(true));
// now check what happens if we get back an unacknowledged response
try {
listeners.get(0).onResponse(new TestPutMappingResponse());
fail("this hould have failed because request was not acknowledged");
this.listeners.get(0).onResponse(new TestPutMappingResponse());
fail("this should have failed because request was not acknowledged");
} catch (ElasticsearchException e) {
}
assertThat(securityIndex.isMappingUpdatePending(), equalTo(false));
// and now check what happens if we get back an acknowledged response
listeners.clear();
this.listeners.clear();
securityLifecycleService.clusterChanged(new ClusterChangedEvent("test-event",
clusterStateBuilder.build(), EMPTY_CLUSTER_STATE));
assertThat(listeners.size(), equalTo(expectedNumberOfListeners));
assertThat(this.listeners.size(), equalTo(numberOfSecurityTypes));
int counter = 0;
for (ActionListener actionListener : listeners) {
for (ActionListener actionListener : this.listeners) {
actionListener.onResponse(new TestPutMappingResponse(true));
if (++counter < expectedNumberOfListeners) {
if (++counter < numberOfSecurityTypes) {
assertThat(securityIndex.isMappingUpdatePending(), equalTo(true));
} else {
assertThat(securityIndex.isMappingUpdatePending(), equalTo(false));
@ -293,9 +303,10 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
}
}
public void testUpToDateMappingIsIdentifiedAstUpToDate() throws IOException {
String templateString = "/" + SECURITY_TEMPLATE_NAME + ".json";
ClusterState.Builder clusterStateBuilder = createClusterStateWithMapping(templateString);
public void testUpToDateMappingsAreIdentifiedAsUpToDate() throws IOException {
String securityTemplateString = "/" + SECURITY_TEMPLATE_NAME + ".json";
ClusterState.Builder clusterStateBuilder = createClusterStateWithMapping(
securityTemplateString);
securityLifecycleService.clusterChanged(new ClusterChangedEvent("test-event",
clusterStateBuilder.build(), EMPTY_CLUSTER_STATE));
assertTrue(securityLifecycleService.securityIndex().isMappingUpToDate());
@ -304,7 +315,8 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
public void testMappingVersionMatching() throws IOException {
String templateString = "/" + SECURITY_TEMPLATE_NAME + ".json";
ClusterState.Builder clusterStateBuilder = createClusterStateWithMapping(templateString);
ClusterState.Builder clusterStateBuilder = createClusterStateWithMapping(templateString
);
securityLifecycleService.clusterChanged(new ClusterChangedEvent("test-event",
clusterStateBuilder.build(), EMPTY_CLUSTER_STATE));
final IndexLifecycleManager securityIndex = securityLifecycleService.securityIndex();
@ -314,18 +326,19 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
public void testMissingVersionMappingThrowsError() throws IOException {
String templateString = "/missing-version-" + SECURITY_TEMPLATE_NAME + ".json";
ClusterState.Builder clusterStateBuilder = createClusterStateWithMapping(templateString);
ClusterState.Builder clusterStateBuilder = createClusterStateWithMapping(templateString
);
final ClusterState clusterState = clusterStateBuilder.build();
IllegalStateException exception = expectThrows(IllegalStateException.class,
() -> securityIndexMappingAndTemplateUpToDate(clusterState, logger));
assertEquals(exception.getMessage(), "Cannot read security-version string");
assertEquals(exception.getMessage(), "Cannot read security-version string in index " + SECURITY_INDEX_NAME);
}
public void testMissingIndexIsIdentifiedAsUpToDate() throws IOException {
final ClusterName clusterName = new ClusterName("test-cluster");
final ClusterState.Builder clusterStateBuilder = ClusterState.builder(clusterName);
String mappingString = "/" + SECURITY_TEMPLATE_NAME + ".json";
IndexTemplateMetaData.Builder templateMeta = getIndexTemplateMetaData(mappingString);
IndexTemplateMetaData.Builder templateMeta = getIndexTemplateMetaData(SECURITY_TEMPLATE_NAME, mappingString);
MetaData.Builder builder = new MetaData.Builder(clusterStateBuilder.build().getMetaData());
builder.put(templateMeta);
clusterStateBuilder.metaData(builder);
@ -336,29 +349,30 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
assertThat(listeners.size(), equalTo(0));
}
private ClusterState.Builder createClusterStateWithMapping(String templateString)
throws IOException {
IndexMetaData.Builder indexMetaData = createIndexMetadata(templateString);
private ClusterState.Builder createClusterStateWithMapping(String securityTemplateString) throws IOException {
ImmutableOpenMap.Builder mapBuilder = ImmutableOpenMap.builder();
mapBuilder.put(SECURITY_INDEX_NAME, indexMetaData.build());
IndexMetaData securityIndex = createIndexMetadata(SECURITY_INDEX_NAME, securityTemplateString).build();
mapBuilder.put(SECURITY_INDEX_NAME, securityIndex);
MetaData.Builder metaDataBuilder = new MetaData.Builder();
metaDataBuilder.indices(mapBuilder.build());
String mappingString = "/" + SECURITY_TEMPLATE_NAME + ".json";
IndexTemplateMetaData.Builder templateMeta = getIndexTemplateMetaData(mappingString);
metaDataBuilder.put(templateMeta);
String securityMappingString = "/" + SECURITY_TEMPLATE_NAME + ".json";
IndexTemplateMetaData.Builder securityTemplateMeta = getIndexTemplateMetaData(SECURITY_TEMPLATE_NAME, securityMappingString);
metaDataBuilder.put(securityTemplateMeta);
ClusterState.Builder clusterStateBuilder = ClusterState.builder(state());
final RoutingTable routingTable = SecurityTestUtils.buildSecurityIndexRoutingTable();
clusterStateBuilder.metaData(metaDataBuilder.build()).routingTable(routingTable);
return clusterStateBuilder;
}
private static IndexMetaData.Builder createIndexMetadata(String templateString)
throws IOException {
private static IndexMetaData.Builder createIndexMetadata(
String indexName, String templateString) throws IOException {
String template = TemplateUtils.loadTemplate(templateString, Version.CURRENT.toString(),
IndexLifecycleManager.TEMPLATE_VERSION_PATTERN);
PutIndexTemplateRequest request = new PutIndexTemplateRequest();
request.source(template, XContentType.JSON);
IndexMetaData.Builder indexMetaData = IndexMetaData.builder(SECURITY_INDEX_NAME);
IndexMetaData.Builder indexMetaData = IndexMetaData.builder(indexName);
indexMetaData.settings(Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
@ -371,27 +385,30 @@ public class SecurityLifecycleServiceTests extends ESTestCase {
return indexMetaData;
}
public static ClusterState.Builder createClusterStateWithTemplate(String templateString)
throws IOException {
IndexTemplateMetaData.Builder templateBuilder = getIndexTemplateMetaData(templateString);
MetaData.Builder metaDataBuidler = new MetaData.Builder();
metaDataBuidler.put(templateBuilder);
public static ClusterState.Builder createClusterStateWithTemplate(String securityTemplateString) throws IOException {
MetaData.Builder metaDataBuilder = new MetaData.Builder();
IndexTemplateMetaData.Builder securityTemplateBuilder =
getIndexTemplateMetaData(SECURITY_TEMPLATE_NAME, securityTemplateString);
metaDataBuilder.put(securityTemplateBuilder);
// add the correct mapping no matter what the template
String mappingString = "/" + SECURITY_TEMPLATE_NAME + ".json";
IndexMetaData.Builder indexMeta = createIndexMetadata(mappingString);
metaDataBuidler.put(indexMeta);
return ClusterState.builder(state())
.metaData(metaDataBuidler.build());
String securityMappingString = "/" + SECURITY_TEMPLATE_NAME + ".json";
IndexMetaData.Builder securityIndexMeta =
createIndexMetadata(SECURITY_INDEX_NAME, securityMappingString);
metaDataBuilder.put(securityIndexMeta);
return ClusterState.builder(state()).metaData(metaDataBuilder.build());
}
private static IndexTemplateMetaData.Builder getIndexTemplateMetaData(String templateString)
throws IOException {
private static IndexTemplateMetaData.Builder getIndexTemplateMetaData(
String templateName, String templateString) throws IOException {
String template = TemplateUtils.loadTemplate(templateString, Version.CURRENT.toString(),
IndexLifecycleManager.TEMPLATE_VERSION_PATTERN);
PutIndexTemplateRequest request = new PutIndexTemplateRequest();
request.source(template, XContentType.JSON);
IndexTemplateMetaData.Builder templateBuilder =
IndexTemplateMetaData.builder(SECURITY_TEMPLATE_NAME);
IndexTemplateMetaData.builder(templateName);
for (Map.Entry<String, String> entry : request.mappings().entrySet()) {
templateBuilder.putMapping(entry.getKey(), entry.getValue());
}

View File

@ -149,8 +149,8 @@ public class SecuritySettingsTests extends ESTestCase {
assertThat(e.getMessage(), not(containsString(IndexAuditTrail.INDEX_NAME_PREFIX)));
}
Security.validateAutoCreateIndex(Settings.builder()
.putArray("action.auto_create_index", ".security", ".security-invalidated-tokens").build());
Security.validateAutoCreateIndex(Settings.builder().put("action.auto_create_index", ".security").build());
Security.validateAutoCreateIndex(Settings.builder().put("action.auto_create_index", ".security*").build());
Security.validateAutoCreateIndex(Settings.builder().put("action.auto_create_index", "*s*").build());
Security.validateAutoCreateIndex(Settings.builder().put("action.auto_create_index", ".s*").build());
@ -170,7 +170,7 @@ public class SecuritySettingsTests extends ESTestCase {
}
Security.validateAutoCreateIndex(Settings.builder()
.putArray("action.auto_create_index", ".security", ".security-invalidated-tokens")
.put("action.auto_create_index", ".security")
.put(XPackSettings.AUDIT_ENABLED.getKey(), true)
.build());
@ -187,7 +187,7 @@ public class SecuritySettingsTests extends ESTestCase {
}
Security.validateAutoCreateIndex(Settings.builder()
.put("action.auto_create_index", ".security_audit_log*,.security,.security-invalidated-tokens")
.put("action.auto_create_index", ".security_audit_log*,.security")
.put(XPackSettings.AUDIT_ENABLED.getKey(), true)
.put(Security.AUDIT_OUTPUTS_SETTING.getKey(), randomFrom("index", "logfile,index"))
.build());

View File

@ -0,0 +1,70 @@
/*
* 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.rolemapping;
import java.util.Collections;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.ValidationException;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
import org.junit.Before;
import org.mockito.Mockito;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.notNullValue;
public class PutRoleMappingRequestTests extends ESTestCase {
private PutRoleMappingRequestBuilder builder;
@Before
public void setupBuilder() {
final ElasticsearchClient client = Mockito.mock(ElasticsearchClient.class);
builder = new PutRoleMappingRequestBuilder(client, PutRoleMappingAction.INSTANCE);
}
public void testValidateMissingName() throws Exception {
final PutRoleMappingRequest request = builder
.roles("superuser")
.expression(Mockito.mock(RoleMapperExpression.class))
.request();
assertValidationFailure(request, "name");
}
public void testValidateMissingRoles() throws Exception {
final PutRoleMappingRequest request = builder
.name("test")
.expression(Mockito.mock(RoleMapperExpression.class))
.request();
assertValidationFailure(request, "roles");
}
public void testValidateMissingRules() throws Exception {
final PutRoleMappingRequest request = builder
.name("test")
.roles("superuser")
.request();
assertValidationFailure(request, "rules");
}
public void testValidateMetadataKeys() throws Exception {
final PutRoleMappingRequest request = builder
.name("test")
.roles("superuser")
.expression(Mockito.mock(RoleMapperExpression.class))
.metadata(Collections.singletonMap("_secret", false))
.request();
assertValidationFailure(request, "metadata key");
}
private void assertValidationFailure(PutRoleMappingRequest request, String expectedMessage) {
final ValidationException ve = request.validate();
assertThat(ve, notNullValue());
assertThat(ve.getMessage(), containsString(expectedMessage));
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.rolemapping;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.hamcrest.Matchers;
import org.junit.Before;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
public class TransportGetRoleMappingsActionTests extends ESTestCase {
private NativeRoleMappingStore store;
private TransportGetRoleMappingsAction action;
private AtomicReference<Set<String>> namesRef;
private List<ExpressionRoleMapping> result;
@Before
public void setupMocks() {
store = mock(NativeRoleMappingStore.class);
TransportService transportService = new TransportService(Settings.EMPTY, null, null,
TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null);
action = new TransportGetRoleMappingsAction(Settings.EMPTY, mock(ThreadPool.class),
mock(ActionFilters.class), mock(IndexNameExpressionResolver.class),
transportService, store);
namesRef = new AtomicReference<>(null);
result = Collections.emptyList();
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
assert args.length == 2;
namesRef.set((Set<String>) args[0]);
ActionListener<List<ExpressionRoleMapping>> listener = (ActionListener) args[1];
listener.onResponse(result);
return null;
}).when(store).getRoleMappings(any(Set.class), any(ActionListener.class));
}
public void testGetSingleRole() throws Exception {
final PlainActionFuture<GetRoleMappingsResponse> future = new PlainActionFuture<>();
final GetRoleMappingsRequest request = new GetRoleMappingsRequest();
request.setNames("everyone");
final ExpressionRoleMapping mapping = mock(ExpressionRoleMapping.class);
result = Collections.singletonList(mapping);
action.doExecute(request, future);
assertThat(future.get(), notNullValue());
assertThat(future.get().mappings(), arrayContaining(mapping));
assertThat(namesRef.get(), containsInAnyOrder("everyone"));
}
public void testGetMultipleNamedRoles() throws Exception {
final PlainActionFuture<GetRoleMappingsResponse> future = new PlainActionFuture<>();
final GetRoleMappingsRequest request = new GetRoleMappingsRequest();
request.setNames("admin", "engineering", "sales", "finance");
final ExpressionRoleMapping mapping1 = mock(ExpressionRoleMapping.class);
final ExpressionRoleMapping mapping2 = mock(ExpressionRoleMapping.class);
final ExpressionRoleMapping mapping3 = mock(ExpressionRoleMapping.class);
result = Arrays.asList(mapping1, mapping2, mapping3);
action.doExecute(request, future);
final GetRoleMappingsResponse response = future.get();
assertThat(response, notNullValue());
assertThat(response.mappings(), arrayContainingInAnyOrder(mapping1, mapping2, mapping3));
assertThat(namesRef.get(), containsInAnyOrder("admin", "engineering", "sales", "finance"));
}
public void testGetAllRoles() throws Exception {
final PlainActionFuture<GetRoleMappingsResponse> future = new PlainActionFuture<>();
final GetRoleMappingsRequest request = new GetRoleMappingsRequest();
request.setNames(Strings.EMPTY_ARRAY);
final ExpressionRoleMapping mapping1 = mock(ExpressionRoleMapping.class);
final ExpressionRoleMapping mapping2 = mock(ExpressionRoleMapping.class);
final ExpressionRoleMapping mapping3 = mock(ExpressionRoleMapping.class);
result = Arrays.asList(mapping1, mapping2, mapping3);
action.doExecute(request, future);
final GetRoleMappingsResponse response = future.get();
assertThat(response, notNullValue());
assertThat(response.mappings(), arrayContainingInAnyOrder(mapping1, mapping2, mapping3));
assertThat(namesRef.get(), Matchers.nullValue(Set.class));
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.rolemapping;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.FieldExpression;
import org.junit.Before;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
public class TransportPutRoleMappingActionTests extends ESTestCase {
private NativeRoleMappingStore store;
private TransportPutRoleMappingAction action;
private AtomicReference<PutRoleMappingRequest> requestRef;
@Before
public void setupMocks() {
store = mock(NativeRoleMappingStore.class);
TransportService transportService = new TransportService(Settings.EMPTY, null, null,
TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null);
action = new TransportPutRoleMappingAction(Settings.EMPTY, mock(ThreadPool.class),
mock(ActionFilters.class), mock(IndexNameExpressionResolver.class),
transportService, store);
requestRef = new AtomicReference<>(null);
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
assert args.length == 2;
requestRef.set((PutRoleMappingRequest) args[0]);
ActionListener<Boolean> listener = (ActionListener) args[1];
listener.onResponse(true);
return null;
}).when(store).putRoleMapping(any(PutRoleMappingRequest.class), any(ActionListener.class)
);
}
public void testPutValidMapping() throws Exception {
final FieldExpression expression = new FieldExpression(
"username",
Collections.singletonList(FieldExpression.FieldPredicate.create("*"))
);
final PutRoleMappingResponse response = put("anarchy", expression, "superuser",
Collections.singletonMap("dumb", true));
assertThat(response.isCreated(), equalTo(true));
final ExpressionRoleMapping mapping = requestRef.get().getMapping();
assertThat(mapping.getExpression(), is(expression));
assertThat(mapping.isEnabled(), equalTo(true));
assertThat(mapping.getName(), equalTo("anarchy"));
assertThat(mapping.getRoles(), containsInAnyOrder("superuser"));
assertThat(mapping.getMetadata().size(), equalTo(1));
assertThat(mapping.getMetadata().get("dumb"), equalTo(true));
}
private PutRoleMappingResponse put(String name, FieldExpression expression, String role,
Map<String, Object> metadata) throws Exception {
final PutRoleMappingRequest request = new PutRoleMappingRequest();
request.setName(name);
request.setRoles(Arrays.asList(role));
request.setRules(expression);
request.setMetadata(metadata);
request.setEnabled(true);
final PlainActionFuture<PutRoleMappingResponse> future = new PlainActionFuture<>();
action.doExecute(request, future);
return future.get();
}
}

View File

@ -5,21 +5,6 @@
*/
package org.elasticsearch.xpack.security.authc.pki;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.ssl.SSLClientAuth;
import org.elasticsearch.xpack.ssl.SSLService;
import org.elasticsearch.xpack.security.user.User;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.support.DnRoleMapper;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.security.support.NoOpLogger;
import org.elasticsearch.test.ESTestCase;
import org.junit.Before;
import javax.security.auth.x500.X500Principal;
import java.io.InputStream;
import java.nio.file.Files;
@ -27,14 +12,34 @@ import java.nio.file.Path;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.security.support.NoOpLogger;
import org.elasticsearch.xpack.security.user.User;
import org.elasticsearch.xpack.ssl.SSLClientAuth;
import org.elasticsearch.xpack.ssl.SSLService;
import org.junit.Before;
import org.mockito.Mockito;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Matchers.anyList;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -56,7 +61,7 @@ public class PkiRealmTests extends ESTestCase {
public void testTokenSupport() {
RealmConfig config = new RealmConfig("", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings));
PkiRealm realm = new PkiRealm(config, mock(DnRoleMapper.class), sslService);
PkiRealm realm = new PkiRealm(config, mock(UserRoleMapper.class), sslService);
assertThat(realm.supports(null), is(false));
assertThat(realm.supports(new UsernamePasswordToken("", new SecureString(new char[0]))), is(false));
@ -67,7 +72,8 @@ public class PkiRealmTests extends ESTestCase {
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate });
PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)), mock(DnRoleMapper.class), sslService);
PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.EMPTY, globalSettings, new Environment(globalSettings),
new ThreadContext(globalSettings)), mock(UserRoleMapper.class), sslService);
X509AuthenticationToken token = realm.token(threadContext);
assertThat(token, is(notNullValue()));
@ -76,12 +82,33 @@ public class PkiRealmTests extends ESTestCase {
}
public void testAuthenticateBasedOnCertToken() throws Exception {
assertSuccessfulAuthentiation(Collections.emptySet());
}
public void testAuthenticateWithRoleMapping() throws Exception {
final Set<String> roles = new HashSet<>();
roles.add("admin");
roles.add("kibana_user");
assertSuccessfulAuthentiation(roles);
}
private void assertSuccessfulAuthentiation(Set<String> roles) throws Exception {
String dn = "CN=Elasticsearch Test Node,";
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
X509AuthenticationToken token = new X509AuthenticationToken(new X509Certificate[] { certificate }, "Elasticsearch Test Node",
"CN=Elasticsearch Test Node,");
DnRoleMapper roleMapper = mock(DnRoleMapper.class);
PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)), roleMapper, sslService);
when(roleMapper.resolveRoles(anyString(), anyList())).thenReturn(Collections.<String>emptySet());
X509AuthenticationToken token = new X509AuthenticationToken(new X509Certificate[] { certificate }, "Elasticsearch Test Node", dn);
UserRoleMapper roleMapper = mock(UserRoleMapper.class);
PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.EMPTY, globalSettings, new Environment(globalSettings),
new ThreadContext(globalSettings)), roleMapper, sslService);
Mockito.doAnswer(invocation -> {
final UserRoleMapper.UserData userData = (UserRoleMapper.UserData) invocation.getArguments()[0];
final ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1];
if (userData.getDn().equals(dn)) {
listener.onResponse(roles);
} else {
listener.onFailure(new IllegalArgumentException("Expected DN '" + dn + "' but was '" + userData + "'"));
}
return null;
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
PlainActionFuture<User> future = new PlainActionFuture<>();
realm.authenticate(token, future);
@ -89,15 +116,20 @@ public class PkiRealmTests extends ESTestCase {
assertThat(user, is(notNullValue()));
assertThat(user.principal(), is("Elasticsearch Test Node"));
assertThat(user.roles(), is(notNullValue()));
assertThat(user.roles().length, is(0));
assertThat(user.roles().length, is(roles.size()));
assertThat(user.roles(), arrayContainingInAnyOrder(roles.toArray()));
}
public void testCustomUsernamePattern() throws Exception {
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
DnRoleMapper roleMapper = mock(DnRoleMapper.class);
UserRoleMapper roleMapper = mock(UserRoleMapper.class);
PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.builder().put("username_pattern", "OU=(.*?),").build(), globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)),
roleMapper, sslService);
when(roleMapper.resolveRoles(anyString(), anyList())).thenReturn(Collections.<String>emptySet());
Mockito.doAnswer(invocation -> {
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1];
listener.onResponse(Collections.emptySet());
return null;
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate });
@ -113,13 +145,19 @@ public class PkiRealmTests extends ESTestCase {
public void testVerificationUsingATruststore() throws Exception {
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
DnRoleMapper roleMapper = mock(DnRoleMapper.class);
UserRoleMapper roleMapper = mock(UserRoleMapper.class);
Settings settings = Settings.builder()
.put("truststore.path", getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"))
.put("truststore.password", "testnode")
.build();
PkiRealm realm = new PkiRealm(new RealmConfig("", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)), roleMapper, sslService);
when(roleMapper.resolveRoles(anyString(), anyList())).thenReturn(Collections.<String>emptySet());
PkiRealm realm = new PkiRealm(new RealmConfig("", settings, globalSettings, new Environment(globalSettings),
new ThreadContext(globalSettings)), roleMapper, sslService);
Mockito.doAnswer(invocation -> {
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1];
listener.onResponse(Collections.emptySet());
return null;
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate });
@ -136,14 +174,19 @@ public class PkiRealmTests extends ESTestCase {
public void testVerificationFailsUsingADifferentTruststore() throws Exception {
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
DnRoleMapper roleMapper = mock(DnRoleMapper.class);
UserRoleMapper roleMapper = mock(UserRoleMapper.class);
Settings settings = Settings.builder()
.put("truststore.path",
getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode-client-profile.jks"))
.put("truststore.password", "testnode-client-profile")
.build();
PkiRealm realm = new PkiRealm(new RealmConfig("", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)), roleMapper, sslService);
when(roleMapper.resolveRoles(anyString(), anyList())).thenReturn(Collections.<String>emptySet());
PkiRealm realm = new PkiRealm(new RealmConfig("", settings, globalSettings, new Environment(globalSettings),
new ThreadContext(globalSettings)), roleMapper, sslService);
Mockito.doAnswer(invocation -> {
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1];
listener.onResponse(Collections.emptySet());
return null;
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate });
@ -161,7 +204,8 @@ public class PkiRealmTests extends ESTestCase {
getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode-client-profile.jks"))
.build();
try {
new PkiRealm(new RealmConfig("mypki", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)), mock(DnRoleMapper.class), sslService);
new PkiRealm(new RealmConfig("mypki", settings, globalSettings, new Environment(globalSettings),
new ThreadContext(globalSettings)), mock(UserRoleMapper.class), sslService);
fail("exception should have been thrown");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("[xpack.security.authc.realms.mypki.truststore.password] is not configured"));
@ -173,7 +217,7 @@ public class PkiRealmTests extends ESTestCase {
X500Principal principal = new X500Principal("CN=PKI Client");
when(certificate.getSubjectX500Principal()).thenReturn(principal);
X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate},
X509AuthenticationToken token = PkiRealm.token(new X509Certificate[] { certificate },
Pattern.compile(PkiRealm.DEFAULT_USERNAME_PATTERN), NoOpLogger.INSTANCE);
assertThat(token, notNullValue());
assertThat(token.principal(), is("PKI Client"));
@ -185,7 +229,7 @@ public class PkiRealmTests extends ESTestCase {
X500Principal principal = new X500Principal("CN=PKI Client, OU=Security");
when(certificate.getSubjectX500Principal()).thenReturn(principal);
X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate},
X509AuthenticationToken token = PkiRealm.token(new X509Certificate[] { certificate },
Pattern.compile(PkiRealm.DEFAULT_USERNAME_PATTERN), NoOpLogger.INSTANCE);
assertThat(token, notNullValue());
assertThat(token.principal(), is("PKI Client"));
@ -197,7 +241,7 @@ public class PkiRealmTests extends ESTestCase {
X500Principal principal = new X500Principal("EMAILADDRESS=pki@elastic.co, CN=PKI Client, OU=Security");
when(certificate.getSubjectX500Principal()).thenReturn(principal);
X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate},
X509AuthenticationToken token = PkiRealm.token(new X509Certificate[] { certificate },
Pattern.compile(PkiRealm.DEFAULT_USERNAME_PATTERN), NoOpLogger.INSTANCE);
assertThat(token, notNullValue());
assertThat(token.principal(), is("PKI Client"));
@ -211,8 +255,8 @@ public class PkiRealmTests extends ESTestCase {
.build();
IllegalStateException e = expectThrows(IllegalStateException.class,
() -> new PkiRealm(new RealmConfig("", Settings.EMPTY, settings, new Environment(settings), new ThreadContext(settings)), mock(DnRoleMapper.class),
new SSLService(settings, new Environment(settings))));
() -> new PkiRealm(new RealmConfig("", Settings.EMPTY, settings, new Environment(settings), new ThreadContext(settings)),
mock(UserRoleMapper.class), new SSLService(settings, new Environment(settings))));
assertThat(e.getMessage(), containsString("has SSL with client authentication enabled"));
}
@ -223,8 +267,8 @@ public class PkiRealmTests extends ESTestCase {
.put("xpack.security.http.ssl.enabled", true)
.put("xpack.security.http.ssl.client_authentication", randomFrom(SSLClientAuth.OPTIONAL, SSLClientAuth.REQUIRED))
.build();
new PkiRealm(new RealmConfig("", Settings.EMPTY, settings, new Environment(settings), new ThreadContext(settings)), mock(DnRoleMapper.class),
new SSLService(settings, new Environment(settings)));
new PkiRealm(new RealmConfig("", Settings.EMPTY, settings, new Environment(settings), new ThreadContext(settings)),
mock(UserRoleMapper.class), new SSLService(settings, new Environment(settings)));
}
static X509Certificate readCert(Path path) throws Exception {

View File

@ -0,0 +1,130 @@
/*
* 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.
*/
/*
* ELASTICSEARCH CONFIDENTIAL
* __________________
*
* [2017] Elasticsearch Incorporated. All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Elasticsearch Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Elasticsearch Incorporated
* and its suppliers and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Elasticsearch Incorporated.
*/
package org.elasticsearch.xpack.security.authc.support.mapper;
import java.io.IOException;
import java.util.Collections;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.AllExpression;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.mockito.Mockito;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
public class ExpressionRoleMappingTests extends ESTestCase {
private RealmConfig realm;
@Before
public void setupMapping() throws Exception {
realm = new RealmConfig("ldap1", Settings.EMPTY, Settings.EMPTY, Mockito.mock(Environment.class),
new ThreadContext(Settings.EMPTY));
}
public void testParseValidJson() throws Exception {
String json = "{"
+ "\"roles\": [ \"kibana_user\", \"sales\" ], "
+ "\"enabled\": true, "
+ "\"rules\": { "
+ " \"all\": [ "
+ " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } }, "
+ " { \"except\": { \"field\": { \"metadata.active\" : false } } }"
+ " ]}"
+ "}";
final ExpressionRoleMapping mapping = parse(json, "ldap_sales");
assertThat(mapping.getRoles(), Matchers.containsInAnyOrder("kibana_user", "sales"));
assertThat(mapping.getExpression(), instanceOf(AllExpression.class));
final UserRoleMapper.UserData user1 = new UserRoleMapper.UserData(
"john.smith", "cn=john.smith,ou=sales,dc=example,dc=com",
Collections.emptyList(), Collections.singletonMap("active", true), realm
);
final UserRoleMapper.UserData user2 = new UserRoleMapper.UserData(
"jamie.perez", "cn=jamie.perez,ou=sales,dc=example,dc=com",
Collections.emptyList(), Collections.singletonMap("active", false), realm
);
final UserRoleMapper.UserData user3 = new UserRoleMapper.UserData(
"simone.ng", "cn=simone.ng,ou=finance,dc=example,dc=com",
Collections.emptyList(), Collections.singletonMap("active", true), realm
);
assertThat(mapping.getExpression().match(user1.asMap()), equalTo(true));
assertThat(mapping.getExpression().match(user2.asMap()), equalTo(false));
assertThat(mapping.getExpression().match(user3.asMap()), equalTo(false));
}
public void testParsingFailsIfRulesAreMissing() throws Exception {
String json = "{"
+ "\"roles\": [ \"kibana_user\", \"sales\" ], "
+ "\"enabled\": true "
+ "}";
ParsingException ex = expectThrows(ParsingException.class, () -> parse(json, "bad_json"));
assertThat(ex.getMessage(), containsString("rules"));
}
public void testParsingFailsIfRolesMissing() throws Exception {
String json = "{"
+ "\"enabled\": true, "
+ "\"rules\": "
+ " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } } "
+ "}";
ParsingException ex = expectThrows(ParsingException.class, () -> parse(json, "bad_json"));
assertThat(ex.getMessage(), containsString("role"));
}
public void testParsingFailsIfThereAreUnrecognisedFields() throws Exception {
String json = "{"
+ "\"disabled\": false, "
+ "\"roles\": [ \"kibana_user\", \"sales\" ], "
+ "\"rules\": "
+ " { \"field\": { \"dn\" : \"*,ou=sales,dc=example,dc=com\" } } "
+ "}";
ParsingException ex = expectThrows(ParsingException.class, () -> parse(json, "bad_json"));
assertThat(ex.getMessage(), containsString("disabled"));
}
private ExpressionRoleMapping parse(String json, String name) throws IOException {
final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY;
final XContentParser parser = XContentType.JSON.xContent().createParser(registry, json);
final ExpressionRoleMapping mapping = ExpressionRoleMapping.parse(name, parser);
assertThat(mapping, notNullValue());
assertThat(mapping.getName(), equalTo(name));
return mapping;
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.authc.support.mapper;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.FieldExpression;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.FieldExpression.FieldPredicate;
import org.hamcrest.Matchers;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeUserRoleMapperTests extends ESTestCase {
public void testResolveRoles() throws Exception {
// Does match DN
final ExpressionRoleMapping mapping1 = new ExpressionRoleMapping("dept_h",
new FieldExpression("dn", Collections.singletonList(FieldPredicate.create("*,ou=dept_h,o=forces,dc=gc,dc=ca"))),
Arrays.asList("dept_h", "defence"), Collections.emptyMap(), true);
// Does not match - user is not in this group
final ExpressionRoleMapping mapping2 = new ExpressionRoleMapping("admin",
new FieldExpression("groups",
Collections.singletonList(FieldPredicate.create("cn=esadmin,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"))),
Arrays.asList("admin"), Collections.emptyMap(), true);
// Does match - user is one of these groups
final ExpressionRoleMapping mapping3 = new ExpressionRoleMapping("flight",
new FieldExpression("groups", Arrays.asList(
FieldPredicate.create("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"),
FieldPredicate.create("cn=betaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"),
FieldPredicate.create("cn=gammaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")
)),
Arrays.asList("flight"), Collections.emptyMap(), true);
// Does not match - mapping is not enabled
final ExpressionRoleMapping mapping4 = new ExpressionRoleMapping("mutants",
new FieldExpression("groups",
Collections.singletonList(FieldPredicate.create("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"))),
Arrays.asList("mutants"), Collections.emptyMap(), false);
final InternalClient client = mock(InternalClient.class);
final SecurityLifecycleService lifecycleService = mock(SecurityLifecycleService.class);
when(lifecycleService.isSecurityIndexAvailable()).thenReturn(true);
final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, lifecycleService) {
@Override
protected void loadMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
listener.onResponse(Arrays.asList(mapping1, mapping2, mapping3, mapping4));
}
};
final RealmConfig realm = new RealmConfig("ldap1", Settings.EMPTY, Settings.EMPTY, mock(Environment.class),
new ThreadContext(Settings.EMPTY));
final PlainActionFuture<Set<String>> future = new PlainActionFuture<>();
final UserRoleMapper.UserData user = new UserRoleMapper.UserData("sasquatch",
"cn=walter.langowski,ou=people,ou=dept_h,o=forces,dc=gc,dc=ca",
Arrays.asList(
"cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca",
"cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"
), Collections.emptyMap(), realm);
store.resolveRoles(user, future);
final Set<String> roles = future.get();
assertThat(roles, Matchers.containsInAnyOrder("dept_h", "defence", "flight"));
}
}

View File

@ -0,0 +1,162 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import com.carrotsearch.randomizedtesting.WriterOutputStream;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.watcher.support.xcontent.XContentSource;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.iterableWithSize;
public class ExpressionParserTests extends ESTestCase {
public void testParseSimpleFieldExpression() throws Exception {
String json = "{ \"field\": { \"username\" : \"*@shield.gov\" } }";
FieldExpression field = checkExpressionType(parse(json), FieldExpression.class);
assertThat(field.getField(), equalTo("username"));
assertThat(field.getValues(), iterableWithSize(1));
final Predicate<Object> predicate = field.getValues().get(0);
assertThat(predicate.test("bob@shield.gov"), equalTo(true));
assertThat(predicate.test("bob@example.net"), equalTo(false));
assertThat(json(field), equalTo(json.replaceAll("\\s", "")));
}
public void testParseComplexExpression() throws Exception {
String json = "{ \"any\": [" +
" { \"field\": { \"username\" : \"*@shield.gov\" } }, " +
" { \"all\": [" +
" { \"field\": { \"username\" : \"/.*\\\\@avengers\\\\.(net|org)/\" } }, " +
" { \"field\": { \"groups\" : [ \"admin\", \"operators\" ] } }, " +
" { \"except\":" +
" { \"field\": { \"groups\" : \"disavowed\" } }" +
" }" +
" ] }" +
"] }";
final RoleMapperExpression expr = parse(json);
assertThat(expr, instanceOf(AnyExpression.class));
AnyExpression any = (AnyExpression) expr;
assertThat(any.getElements(), iterableWithSize(2));
final FieldExpression fieldShield = checkExpressionType(any.getElements().get(0),
FieldExpression.class);
assertThat(fieldShield.getField(), equalTo("username"));
assertThat(fieldShield.getValues(), iterableWithSize(1));
final Predicate<Object> predicateShield = fieldShield.getValues().get(0);
assertThat(predicateShield.test("fury@shield.gov"), equalTo(true));
assertThat(predicateShield.test("fury@shield.net"), equalTo(false));
final AllExpression all = checkExpressionType(any.getElements().get(1),
AllExpression.class);
assertThat(all.getElements(), iterableWithSize(3));
final FieldExpression fieldAvengers = checkExpressionType(all.getElements().get(0),
FieldExpression.class);
assertThat(fieldAvengers.getField(), equalTo("username"));
assertThat(fieldAvengers.getValues(), iterableWithSize(1));
final Predicate<Object> predicateAvengers = fieldAvengers.getValues().get(0);
assertThat(predicateAvengers.test("stark@avengers.net"), equalTo(true));
assertThat(predicateAvengers.test("romanov@avengers.org"), equalTo(true));
assertThat(predicateAvengers.test("fury@shield.gov"), equalTo(false));
final FieldExpression fieldGroupsAdmin = checkExpressionType(all.getElements().get(1),
FieldExpression.class);
assertThat(fieldGroupsAdmin.getField(), equalTo("groups"));
assertThat(fieldGroupsAdmin.getValues(), iterableWithSize(2));
assertThat(fieldGroupsAdmin.getValues().get(0).test("admin"), equalTo(true));
assertThat(fieldGroupsAdmin.getValues().get(0).test("foo"), equalTo(false));
assertThat(fieldGroupsAdmin.getValues().get(1).test("operators"), equalTo(true));
assertThat(fieldGroupsAdmin.getValues().get(1).test("foo"), equalTo(false));
final ExceptExpression except = checkExpressionType(all.getElements().get(2),
ExceptExpression.class);
final FieldExpression fieldDisavowed = checkExpressionType(except.getInnerExpression(),
FieldExpression.class);
assertThat(fieldDisavowed.getField(), equalTo("groups"));
assertThat(fieldDisavowed.getValues(), iterableWithSize(1));
assertThat(fieldDisavowed.getValues().get(0).test("disavowed"), equalTo(true));
assertThat(fieldDisavowed.getValues().get(0).test("_disavowed_"), equalTo(false));
Map<String, Object> hawkeye = new HashMap<>();
hawkeye.put("username", "hawkeye@avengers.org");
hawkeye.put("groups", Arrays.asList("operators"));
assertThat(expr.match(hawkeye), equalTo(true));
Map<String, Object> captain = new HashMap<>();
captain.put("username", "america@avengers.net");
assertThat(expr.match(captain), equalTo(false));
Map<String, Object> warmachine = new HashMap<>();
warmachine.put("username", "warmachine@avengers.net");
warmachine.put("groups", Arrays.asList("admin", "disavowed"));
assertThat(expr.match(warmachine), equalTo(false));
Map<String, Object> fury = new HashMap<>();
fury.put("username", "fury@shield.gov");
fury.put("groups", Arrays.asList("classified", "directors"));
assertThat(expr.asPredicate().test(fury), equalTo(true));
assertThat(json(expr), equalTo(json.replaceAll("\\s", "")));
}
public void testWriteAndReadFromStream() throws IOException {
String json = "{ \"any\": [" +
" { \"field\": { \"username\" : \"*@shield.gov\" } }, " +
" { \"all\": [" +
" { \"field\": { \"username\" : \"/.*\\\\@avengers\\\\.(net|org)/\" } }, " +
" { \"field\": { \"groups\" : [ \"admin\", \"operators\" ] } }, " +
" { \"except\":" +
" { \"field\": { \"groups\" : \"disavowed\" } }" +
" }" +
" ] }" +
"] }";
final RoleMapperExpression exprSource = parse(json);
final BytesStreamOutput out = new BytesStreamOutput();
ExpressionParser.writeExpression(exprSource, out);
final NamedWriteableRegistry registry = new NamedWriteableRegistry(Security.getNamedWriteables());
final NamedWriteableAwareStreamInput input = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), registry);
final RoleMapperExpression exprResult = ExpressionParser.readExpression(input);
assertThat(json(exprResult), equalTo(json.replaceAll("\\s", "")));
}
private <T> T checkExpressionType(RoleMapperExpression expr, Class<T> type) {
assertThat(expr, instanceOf(type));
return type.cast(expr);
}
private RoleMapperExpression parse(String json) throws IOException {
return new ExpressionParser().parse("rules", new XContentSource(new BytesArray(json),
XContentType.JSON));
}
private String json(RoleMapperExpression node) throws IOException {
final StringWriter writer = new StringWriter();
try (XContentBuilder builder = XContentFactory.jsonBuilder(new WriterOutputStream(writer))) {
node.toXContent(builder, ToXContent.EMPTY_PARAMS);
}
return writer.toString();
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.authc.support.mapper.expressiondsl;
import java.math.BigInteger;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authc.support.mapper.expressiondsl.FieldExpression.FieldPredicate;
import static org.hamcrest.Matchers.is;
public class FieldPredicateTests extends ESTestCase {
public void testNullValue() throws Exception {
final FieldPredicate predicate = FieldPredicate.create(null);
assertThat(predicate.test(null), is(true));
assertThat(predicate.test(""), is(false));
assertThat(predicate.test(1), is(false));
assertThat(predicate.test(true), is(false));
}
public void testBooleanValue() throws Exception {
final boolean matchValue = randomBoolean();
final FieldPredicate predicate = FieldPredicate.create(matchValue);
assertThat(predicate.test(matchValue), is(true));
assertThat(predicate.test(!matchValue), is(false));
assertThat(predicate.test(String.valueOf(matchValue)), is(false));
assertThat(predicate.test(""), is(false));
assertThat(predicate.test(1), is(false));
assertThat(predicate.test(null), is(false));
}
public void testLongValue() throws Exception {
final int intValue = randomInt();
final long longValue = intValue;
final FieldPredicate predicate = FieldPredicate.create(longValue);
assertThat(predicate.test(longValue), is(true));
assertThat(predicate.test(intValue), is(true));
assertThat(predicate.test(new BigInteger(String.valueOf(longValue))), is(true));
assertThat(predicate.test(longValue - 1), is(false));
assertThat(predicate.test(intValue + 1), is(false));
assertThat(predicate.test(String.valueOf(longValue)), is(false));
assertThat(predicate.test(""), is(false));
assertThat(predicate.test(true), is(false));
assertThat(predicate.test(null), is(false));
}
public void testSimpleAutomatonValue() throws Exception {
final String prefix = randomAlphaOfLength(3);
final FieldPredicate predicate = FieldPredicate.create(prefix + "*");
assertThat(predicate.test(prefix), is(true));
assertThat(predicate.test(prefix + randomAlphaOfLengthBetween(1, 5)), is(true));
assertThat(predicate.test("_" + prefix), is(false));
assertThat(predicate.test(prefix.substring(0, 1)), is(false));
assertThat(predicate.test(""), is(false));
assertThat(predicate.test(1), is(false));
assertThat(predicate.test(true), is(false));
assertThat(predicate.test(null), is(false));
}
public void testEmptyStringValue() throws Exception {
final FieldPredicate predicate = FieldPredicate.create("");
assertThat(predicate.test(""), is(true));
assertThat(predicate.test(randomAlphaOfLengthBetween(1, 3)), is(false));
assertThat(predicate.test(1), is(false));
assertThat(predicate.test(true), is(false));
assertThat(predicate.test(null), is(false));
}
public void testRegexAutomatonValue() throws Exception {
final String substring = randomAlphaOfLength(5);
final FieldPredicate predicate = FieldPredicate.create("/.*" + substring + ".*/");
assertThat(predicate.test(substring), is(true));
assertThat(predicate.test(
randomAlphaOfLengthBetween(2, 4) + substring + randomAlphaOfLengthBetween(1, 5)),
is(true));
assertThat(predicate.test(substring.substring(1, 3)), is(false));
assertThat(predicate.test(""), is(false));
assertThat(predicate.test(1), is(false));
assertThat(predicate.test(true), is(false));
assertThat(predicate.test(null), is(false));
}
}

View File

@ -93,6 +93,9 @@ cluster:admin/xpack/security/user/has_privileges
cluster:admin/xpack/security/role/put
cluster:admin/xpack/security/role/delete
cluster:admin/xpack/security/role/get
cluster:admin/xpack/security/role_mapping/put
cluster:admin/xpack/security/role_mapping/delete
cluster:admin/xpack/security/role_mapping/get
cluster:admin/xpack/security/token/create
cluster:admin/xpack/security/token/invalidate
cluster:admin/xpack/watcher/service

View File

@ -0,0 +1,25 @@
{
"xpack.security.delete_role_mapping": {
"documentation": "Deletes a native role mapping (Documentation WIP)",
"methods": [ "DELETE" ],
"url": {
"path": "/_xpack/security/role_mapping/{name}",
"paths": [ "/_xpack/security/role_mapping/{name}" ],
"parts": {
"name": {
"type" : "string",
"description" : "Role-mapping name",
"required" : true
}
},
"params": {
"refresh": {
"type" : "enum",
"options": ["true", "false", "wait_for"],
"description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes."
}
}
},
"body": null
}
}

View File

@ -0,0 +1,19 @@
{
"xpack.security.get_role_mapping": {
"documentation": "Retrieves a native role mapping (Documentation WIP)",
"methods": [ "GET" ],
"url": {
"path": "/_xpack/security/role_mapping/{name}",
"paths": [ "/_xpack/security/role_mapping/{name}", "/_xpack/security/role_mapping" ],
"parts": {
"name": {
"type" : "string",
"description" : "Role-Mapping name",
"required" : false
}
},
"params": {}
},
"body": null
}
}

View File

@ -0,0 +1,28 @@
{
"xpack.security.put_role_mapping": {
"documentation": "Stores a native role mapping (Documentation WIP)",
"methods": [ "PUT", "POST" ],
"url": {
"path": "/_xpack/security/role_mapping/{name}",
"paths": [ "/_xpack/security/role_mapping/{name}" ],
"parts": {
"name": {
"type" : "string",
"description" : "Role-mapping name",
"required" : true
}
},
"params": {
"refresh": {
"type" : "enum",
"options": ["true", "false", "wait_for"],
"description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes."
}
}
},
"body": {
"description" : "The role to add",
"required" : true
}
}
}

View File

@ -0,0 +1,46 @@
---
setup:
- skip:
features: headers
- do:
cluster.health:
wait_for_status: yellow
---
teardown:
- do:
xpack.security.delete_role_mapping:
name: "everyone"
ignore: 404
---
"Test put role_mapping api":
- do:
xpack.security.put_role_mapping:
name: "everyone"
body: >
{
"enabled": true,
"roles": [ "kibana_user" ],
"rules": { "field": { "username": "*" } },
"metadata": {
"uuid" : "b9a59ba9-6b92-4be2-bb8d-02bb270cb3a7"
}
}
- match: { role_mapping: { created: true } }
# Get by name
- do:
xpack.security.get_role_mapping:
name: "everyone"
- match: { everyone.enabled: true }
- match: { everyone.roles.0: "kibana_user" }
- match: { everyone.rules.field.username: "*" }
# Get all
- do:
xpack.security.get_role_mapping:
name: null
- match: { everyone.enabled: true }
- match: { everyone.roles.0: "kibana_user" }
- match: { everyone.rules.field.username: "*" }

View File

@ -0,0 +1,12 @@
"Get missing role-mapping":
- do:
catch: missing
xpack.security.get_role_mapping:
name: 'does-not-exist'
---
"Get missing (multiple) role-mappings":
- do:
catch: missing
xpack.security.get_role_mapping:
name: [ 'dne1', 'dne2' ]

View File

@ -0,0 +1,45 @@
---
setup:
- skip:
features: headers
- do:
cluster.health:
wait_for_status: yellow
---
teardown:
- do:
xpack.security.delete_role_mapping:
name: "test_delete"
ignore: 404
---
"Test delete role_mapping api":
- do:
xpack.security.put_role_mapping:
name: "test_delete"
body: >
{
"enabled": true,
"roles": [ "kibana_user" ],
"rules": { "field": { "username": "*" } }
}
- match: { role_mapping: { created: true } }
# Get by name
- do:
xpack.security.get_role_mapping:
name: "test_delete"
- match: { test_delete.enabled: true }
# Delete it
- do:
xpack.security.delete_role_mapping:
name: "test_delete"
- match: { found: true }
# Get by name
- do:
xpack.security.get_role_mapping:
name: "test_delete"
catch: missing