mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-03-24 17:09:48 +00:00
Role Mapping API (elastic/x-pack-elasticsearch#925)
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:
parent
cf27cb479a
commit
6adf4fd3af
@ -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;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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()));
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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]);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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 [{}]",
|
||||
|
@ -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
|
||||
|
@ -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...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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: "*" }
|
@ -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' ]
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user