roles with FLS and/or DLS are ignored when unlicensed (elastic/elasticsearch#4481)

Currently, roles making use of field or document level security are still applied when
the license level does not enable field and document level security. There is no indication
that these roles are not being applied so it is misleading to users. This change prevents
these roles for applying to authorization and also adds a transient metadata to the response
that indicates which features of a role is unlicensed.

Additionally, this PR prevents the addition or modification of roles to include field or
document level security.

Closes elastic/elasticsearch#2472

Original commit: elastic/x-pack-elasticsearch@c9455958f5
This commit is contained in:
Jay Modi 2017-01-03 12:06:33 -05:00 committed by GitHub
parent f4b9e794e8
commit e41b53c344
14 changed files with 599 additions and 63 deletions

View File

@ -7,7 +7,10 @@ package org.elasticsearch.license;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import org.elasticsearch.common.Strings;
@ -176,10 +179,17 @@ public class XPackLicenseState {
}
}
private volatile Status status = new Status(OperationMode.TRIAL, true);
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
/** Updates the current state of the license, which will change what features are available. */
void update(OperationMode mode, boolean active) {
status = new Status(mode, active);
listeners.forEach(Runnable::run);
}
/** Add a listener to be notified on license change */
public void addListener(Runnable runnable) {
listeners.add(Objects.requireNonNull(runnable));
}
/** Return the current license type. */

View File

@ -313,10 +313,14 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
cryptoService, failureHandler, threadPool, anonymousUser));
components.add(authcService.get());
final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService);
final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client);
final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService, licenseState);
final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client, licenseState);
final ReservedRolesStore reservedRolesStore = new ReservedRolesStore();
final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore);
final CompositeRolesStore allRolesStore =
new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, licenseState);
// to keep things simple, just invalidate all cached entries on license change. this happens so rarely that the impact should be
// minimal
licenseState.addListener(allRolesStore::invalidateAll);
final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService,
auditTrailService, failureHandler, threadPool, anonymousUser);
components.add(nativeRolesStore); // used by roles actions

View File

@ -299,7 +299,7 @@ public class ESNativeRealmMigrateTool extends MultiCommand {
static String createRoleJson(RoleDescriptor rd) throws IOException {
XContentBuilder builder = jsonBuilder();
rd.toXContent(builder, ToXContent.EMPTY_PARAMS);
rd.toXContent(builder, ToXContent.EMPTY_PARAMS, false);
return builder.string();
}
@ -312,7 +312,7 @@ public class ESNativeRealmMigrateTool extends MultiCommand {
}
terminal.println("importing roles from [" + rolesFile + "]...");
Logger logger = getTerminalLogger(terminal);
Map<String, RoleDescriptor> roles = FileRolesStore.parseRoleDescriptors(rolesFile, logger, true, env.settings());
Map<String, RoleDescriptor> roles = FileRolesStore.parseRoleDescriptors(rolesFile, logger, true, Settings.EMPTY);
Set<String> existingRoles;
try {
existingRoles = getRolesThatExist(terminal, env.settings(), env, options);

View File

@ -6,9 +6,9 @@
package org.elasticsearch.xpack.security.authz;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.Version;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.ValidationException;
import org.elasticsearch.common.bytes.BytesArray;
@ -44,6 +44,7 @@ public class RoleDescriptor implements ToXContent {
private final IndicesPrivileges[] indicesPrivileges;
private final String[] runAs;
private final Map<String, Object> metadata;
private final Map<String, Object> transientMetadata;
public RoleDescriptor(String name,
@Nullable String[] clusterPrivileges,
@ -57,12 +58,23 @@ public class RoleDescriptor implements ToXContent {
@Nullable IndicesPrivileges[] indicesPrivileges,
@Nullable String[] runAs,
@Nullable Map<String, Object> metadata) {
this(name, clusterPrivileges, indicesPrivileges, runAs, metadata, null);
}
public RoleDescriptor(String name,
@Nullable String[] clusterPrivileges,
@Nullable IndicesPrivileges[] indicesPrivileges,
@Nullable String[] runAs,
@Nullable Map<String, Object> metadata,
@Nullable Map<String, Object> transientMetadata) {
this.name = name;
this.clusterPrivileges = clusterPrivileges != null ? clusterPrivileges : Strings.EMPTY_ARRAY;
this.indicesPrivileges = indicesPrivileges != null ? indicesPrivileges : IndicesPrivileges.NONE;
this.runAs = runAs != null ? runAs : Strings.EMPTY_ARRAY;
this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap();
this.transientMetadata = transientMetadata != null ? Collections.unmodifiableMap(transientMetadata) :
Collections.singletonMap("enabled", true);
}
public String getName() {
@ -85,6 +97,14 @@ public class RoleDescriptor implements ToXContent {
return metadata;
}
public Map<String, Object> getTransientMetadata() {
return transientMetadata;
}
public boolean isUsingDocumentOrFieldLevelSecurity() {
return Arrays.stream(indicesPrivileges).anyMatch(ip -> ip.isUsingDocumentLevelSecurity() || ip.isUsingFieldLevelSecurity());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Role[");
@ -125,7 +145,12 @@ public class RoleDescriptor implements ToXContent {
return result;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return toXContent(builder, params, true);
}
public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean includeTransient) throws IOException {
builder.startObject();
builder.array(Fields.CLUSTER.getPreferredName(), clusterPrivileges);
builder.array(Fields.INDICES.getPreferredName(), (Object[]) indicesPrivileges);
@ -133,6 +158,9 @@ public class RoleDescriptor implements ToXContent {
builder.array(Fields.RUN_AS.getPreferredName(), runAs);
}
builder.field(Fields.METADATA.getPreferredName(), metadata);
if (includeTransient) {
builder.field(Fields.TRANSIENT_METADATA.getPreferredName(), transientMetadata);
}
return builder.endObject();
}
@ -146,7 +174,14 @@ public class RoleDescriptor implements ToXContent {
}
String[] runAs = in.readStringArray();
Map<String, Object> metadata = in.readMap();
return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, runAs, metadata);
final Map<String, Object> transientMetadata;
if (in.getVersion().onOrAfter(Version.V_5_2_0_UNRELEASED)) {
transientMetadata = in.readMap();
} else {
transientMetadata = Collections.emptyMap();
}
return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, runAs, metadata, transientMetadata);
}
public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws IOException {
@ -158,6 +193,9 @@ public class RoleDescriptor implements ToXContent {
}
out.writeStringArray(descriptor.runAs);
out.writeMap(descriptor.metadata);
if (out.getVersion().onOrAfter(Version.V_5_2_0_UNRELEASED)) {
out.writeMap(descriptor.transientMetadata);
}
}
public static RoleDescriptor parse(String name, BytesReference source, boolean allow2xFormat) throws IOException {
@ -202,6 +240,14 @@ public class RoleDescriptor implements ToXContent {
"expected field [{}] to be of type object, but found [{}] instead", currentFieldName, token);
}
metadata = parser.map();
} else if (Fields.TRANSIENT_METADATA.match(currentFieldName)) {
if (token == XContentParser.Token.START_OBJECT) {
// consume object but just drop
parser.map();
} else {
throw new ElasticsearchParseException("expected field [{}] to be an object, but found [{}] instead",
currentFieldName, token);
}
} else {
throw new ElasticsearchParseException("failed to parse role [{}]. unexpected field [{}]", name, currentFieldName);
}
@ -327,6 +373,15 @@ public class RoleDescriptor implements ToXContent {
" permissions in role [{}], use [\"{}\": {\"{}\":[...]," + "\"{}\":[...]}] instead",
roleName, Fields.FIELD_PERMISSIONS, Fields.GRANT_FIELDS, Fields.EXCEPT_FIELDS, roleName);
}
} else if (Fields.TRANSIENT_METADATA.match(currentFieldName)) {
if (token == XContentParser.Token.START_OBJECT) {
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
// it is transient metadata, skip it
}
} else {
throw new ElasticsearchParseException("failed to parse transient metadata for role [{}]. expected {} but got {}" +
" in \"{}\".", roleName, XContentParser.Token.START_OBJECT, token, Fields.TRANSIENT_METADATA);
}
} else {
throw new ElasticsearchParseException("failed to parse indices privileges for role [{}]. unexpected field [{}]",
roleName, currentFieldName);
@ -397,6 +452,30 @@ public class RoleDescriptor implements ToXContent {
return this.query;
}
public boolean isUsingDocumentLevelSecurity() {
return query != null;
}
public boolean isUsingFieldLevelSecurity() {
return hasDeniedFields() || hasGrantedFields();
}
private boolean hasDeniedFields() {
return deniedFields != null && deniedFields.length > 0;
}
private boolean hasGrantedFields() {
if (grantedFields != null && grantedFields.length >= 0) {
// we treat just '*' as no FLS since that's what the UI defaults to
if (grantedFields.length == 1 && "*".equals(grantedFields[0])) {
return false;
} else {
return true;
}
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("IndicesPrivileges[");
@ -562,5 +641,6 @@ public class RoleDescriptor implements ToXContent {
ParseField GRANT_FIELDS = new ParseField("grant");
ParseField EXCEPT_FIELDS = new ParseField("except");
ParseField METADATA = new ParseField("metadata");
ParseField TRANSIENT_METADATA = new ParseField("transient_metadata");
}
}

View File

@ -15,6 +15,7 @@ import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.common.util.concurrent.ReleasableLock;
import org.elasticsearch.common.util.set.Sets;
@ -66,12 +67,13 @@ public class CompositeRolesStore extends AbstractComponent {
private final FileRolesStore fileRolesStore;
private final NativeRolesStore nativeRolesStore;
private final ReservedRolesStore reservedRolesStore;
private final XPackLicenseState licenseState;
private final Cache<Set<String>, Role> roleCache;
private final Set<String> negativeLookupCache;
private final AtomicLong numInvalidation = new AtomicLong();
public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore,
ReservedRolesStore reservedRolesStore) {
ReservedRolesStore reservedRolesStore, XPackLicenseState licenseState) {
super(settings);
this.fileRolesStore = fileRolesStore;
// invalidating all on a file based role update is heavy handed to say the least, but in general this should be infrequent so the
@ -79,6 +81,7 @@ public class CompositeRolesStore extends AbstractComponent {
fileRolesStore.addListener(this::invalidateAll);
this.nativeRolesStore = nativeRolesStore;
this.reservedRolesStore = reservedRolesStore;
this.licenseState = licenseState;
CacheBuilder<Set<String>, Role> builder = CacheBuilder.builder();
final int cacheSize = CACHE_SIZE_SETTING.get(settings);
if (cacheSize >= 0) {
@ -96,7 +99,16 @@ public class CompositeRolesStore extends AbstractComponent {
final long invalidationCounter = numInvalidation.get();
roleDescriptors(roleNames, ActionListener.wrap(
(descriptors) -> {
final Role role = buildRoleFromDescriptors(descriptors, fieldPermissionsCache);
final Role role;
if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) {
role = buildRoleFromDescriptors(descriptors, fieldPermissionsCache);
} else {
final Set<RoleDescriptor> filtered = descriptors.stream()
.filter((rd) -> rd.isUsingDocumentOrFieldLevelSecurity() == false)
.collect(Collectors.toSet());
role = buildRoleFromDescriptors(filtered, fieldPermissionsCache);
}
if (role != null) {
try (ReleasableLock ignored = readLock.acquire()) {
/* this is kinda spooky. We use a read/write lock to ensure we don't modify the cache if we hold the write
@ -161,7 +173,7 @@ public class CompositeRolesStore extends AbstractComponent {
StringBuilder nameBuilder = new StringBuilder();
Set<String> clusterPrivileges = new HashSet<>();
Set<String> runAs = new HashSet<>();
Map<Set<String>, MergableIndicesPrivilege> indicesPrivilegesMap = new HashMap<>();
Map<Set<String>, MergeableIndicesPrivilege> indicesPrivilegesMap = new HashMap<>();
for (RoleDescriptor descriptor : roleDescriptors) {
nameBuilder.append(descriptor.getName());
nameBuilder.append('_');
@ -181,10 +193,10 @@ public class CompositeRolesStore extends AbstractComponent {
if (isExplicitDenial == false) {
indicesPrivilegesMap.compute(key, (k, value) -> {
if (value == null) {
return new MergableIndicesPrivilege(indicesPrivilege.getIndices(), indicesPrivilege.getPrivileges(),
return new MergeableIndicesPrivilege(indicesPrivilege.getIndices(), indicesPrivilege.getPrivileges(),
indicesPrivilege.getGrantedFields(), indicesPrivilege.getDeniedFields(), indicesPrivilege.getQuery());
} else {
value.merge(new MergableIndicesPrivilege(indicesPrivilege.getIndices(), indicesPrivilege.getPrivileges(),
value.merge(new MergeableIndicesPrivilege(indicesPrivilege.getIndices(), indicesPrivilege.getPrivileges(),
indicesPrivilege.getGrantedFields(), indicesPrivilege.getDeniedFields(), indicesPrivilege.getQuery()));
return value;
}
@ -199,7 +211,7 @@ public class CompositeRolesStore extends AbstractComponent {
.cluster(ClusterPrivilege.get(clusterPrivs))
.runAs(runAsPrivilege);
indicesPrivilegesMap.entrySet().forEach((entry) -> {
MergableIndicesPrivilege privilege = entry.getValue();
MergeableIndicesPrivilege privilege = entry.getValue();
builder.add(fieldPermissionsCache.getFieldPermissions(privilege.grantedFields, privilege.deniedFields), privilege.query,
IndexPrivilege.get(privilege.privileges), privilege.indices.toArray(Strings.EMPTY_ARRAY));
});
@ -240,15 +252,15 @@ public class CompositeRolesStore extends AbstractComponent {
/**
* A mutable class that can be used to represent the combination of one or more {@link IndicesPrivileges}
*/
private static class MergableIndicesPrivilege {
private static class MergeableIndicesPrivilege {
private Set<String> indices;
private Set<String> privileges;
private Set<String> grantedFields = null;
private Set<String> deniedFields = null;
private Set<BytesReference> query = null;
MergableIndicesPrivilege(String[] indices, String[] privileges, @Nullable String[] grantedFields, @Nullable String[] deniedFields,
@Nullable BytesReference query) {
MergeableIndicesPrivilege(String[] indices, String[] privileges, @Nullable String[] grantedFields, @Nullable String[] deniedFields,
@Nullable BytesReference query) {
this.indices = Sets.newHashSet(Objects.requireNonNull(indices));
this.privileges = Sets.newHashSet(Objects.requireNonNull(privileges));
this.grantedFields = grantedFields == null ? null : Sets.newHashSet(grantedFields);
@ -258,7 +270,7 @@ public class CompositeRolesStore extends AbstractComponent {
}
}
void merge(MergableIndicesPrivilege other) {
void merge(MergeableIndicesPrivilege other) {
assert indices.equals(other.indices) : "index names must be equivalent in order to merge";
this.grantedFields = combineFieldSets(this.grantedFields, other.grantedFields);
this.deniedFields = combineFieldSets(this.deniedFields, other.deniedFields);

View File

@ -16,6 +16,7 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.yaml.YamlXContent;
import org.elasticsearch.env.Environment;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.watcher.FileChangesListener;
import org.elasticsearch.watcher.FileWatcher;
import org.elasticsearch.watcher.ResourceWatcherService;
@ -40,7 +41,6 @@ import java.util.Set;
import java.util.regex.Pattern;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableMap;
public class FileRolesStore extends AbstractComponent {
@ -49,24 +49,28 @@ public class FileRolesStore extends AbstractComponent {
private static final Pattern SKIP_LINE = Pattern.compile("(^#.*|^\\s*)");
private final Path file;
private final XPackLicenseState licenseState;
private final List<Runnable> listeners = new ArrayList<>();
private volatile Map<String, RoleDescriptor> permissions;
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService) throws IOException {
this(settings, env, watcherService, null);
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, XPackLicenseState licenseState)
throws IOException {
this(settings, env, watcherService, () -> {}, licenseState);
}
FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Runnable listener) throws IOException {
FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Runnable listener,
XPackLicenseState licenseState) throws IOException {
super(settings);
this.file = resolveFile(env);
if (listener != null) {
listeners.add(listener);
}
this.licenseState = licenseState;
FileWatcher watcher = new FileWatcher(file.getParent());
watcher.addListener(new FileListener());
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
permissions = parseFile(file, logger, settings);
permissions = parseFile(file, logger, settings, licenseState);
}
Set<RoleDescriptor> roleDescriptors(Set<String> roleNames) {
@ -113,19 +117,15 @@ public class FileRolesStore extends AbstractComponent {
}
public static Set<String> parseFileForRoleNames(Path path, Logger logger) {
Map<String, RoleDescriptor> roleMap = parseFile(path, logger, false, Settings.EMPTY);
if (roleMap == null) {
return emptySet();
}
return roleMap.keySet();
return parseRoleDescriptors(path, logger, false, Settings.EMPTY).keySet();
}
public static Map<String, RoleDescriptor> parseFile(Path path, Logger logger, Settings settings) {
return parseFile(path, logger, true, settings);
public static Map<String, RoleDescriptor> parseFile(Path path, Logger logger, Settings settings, XPackLicenseState licenseState) {
return parseFile(path, logger, true, settings, licenseState);
}
public static Map<String, RoleDescriptor> parseFile(Path path, Logger logger, boolean resolvePermission,
Settings settings) {
Settings settings, XPackLicenseState licenseState) {
if (logger == null) {
logger = NoOpLogger.INSTANCE;
}
@ -135,18 +135,23 @@ public class FileRolesStore extends AbstractComponent {
if (Files.exists(path)) {
try {
List<String> roleSegments = roleSegments(path);
final boolean flsDlsLicensed = licenseState.isDocumentAndFieldLevelSecurityAllowed();
for (String segment : roleSegments) {
RoleDescriptor descriptor = parseRoleDescriptor(segment, path, logger, resolvePermission, settings);
if (descriptor != null) {
if (ReservedRolesStore.isReserved(descriptor.getName())) {
logger.warn("role [{}] is reserved. the relevant role definition in the mapping file will be ignored",
descriptor.getName());
} else if (flsDlsLicensed == false && descriptor.isUsingDocumentOrFieldLevelSecurity()) {
logger.warn("role [{}] uses document and/or field level security, which is not enabled by the current license" +
". this role will be ignored", descriptor.getName());
// we still put the role in the map to avoid unnecessary negative lookups
roles.put(descriptor.getName(), descriptor);
} else {
roles.put(descriptor.getName(), descriptor);
}
}
}
} catch (IOException ioe) {
logger.error(
(Supplier<?>) () -> new ParameterizedMessage(
@ -164,8 +169,7 @@ public class FileRolesStore extends AbstractComponent {
return unmodifiableMap(roles);
}
public static Map<String, RoleDescriptor> parseRoleDescriptors(Path path, Logger logger,
boolean resolvePermission, Settings settings) {
public static Map<String, RoleDescriptor> parseRoleDescriptors(Path path, Logger logger, boolean resolvePermission, Settings settings) {
if (logger == null) {
logger = NoOpLogger.INSTANCE;
}
@ -194,8 +198,7 @@ public class FileRolesStore extends AbstractComponent {
}
@Nullable
static RoleDescriptor parseRoleDescriptor(String segment, Path path, Logger logger,
boolean resolvePermissions, Settings settings) {
static RoleDescriptor parseRoleDescriptor(String segment, Path path, Logger logger, boolean resolvePermissions, Settings settings) {
String roleName = null;
try {
// EMPTY is safe here because we never use namedObject
@ -312,7 +315,7 @@ public class FileRolesStore extends AbstractComponent {
public void onFileChanged(Path file) {
if (file.equals(FileRolesStore.this.file)) {
try {
permissions = parseFile(file, logger, settings);
permissions = parseFile(file, logger, settings, licenseState);
logger.info("updated roles (roles file [{}] changed)", file.toAbsolutePath());
} catch (Exception e) {
logger.error(

View File

@ -38,6 +38,8 @@ import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.index.get.GetResult;
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.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityTemplateService;
import org.elasticsearch.xpack.security.action.role.ClearRolesCacheRequest;
@ -45,8 +47,10 @@ import org.elasticsearch.xpack.security.action.role.ClearRolesCacheResponse;
import org.elasticsearch.xpack.security.action.role.DeleteRoleRequest;
import org.elasticsearch.xpack.security.action.role.PutRoleRequest;
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges;
import org.elasticsearch.xpack.security.client.SecurityClient;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@ -90,6 +94,7 @@ public class NativeRolesStore extends AbstractComponent implements ClusterStateL
private static final String ROLE_DOC_TYPE = "role";
private final InternalClient client;
private final XPackLicenseState licenseState;
private final AtomicReference<State> state = new AtomicReference<>(State.INITIALIZED);
private final boolean isTribeNode;
@ -98,10 +103,12 @@ public class NativeRolesStore extends AbstractComponent implements ClusterStateL
private volatile boolean securityIndexExists = false;
private volatile boolean canWrite = false;
public NativeRolesStore(Settings settings, InternalClient client) {
public NativeRolesStore(Settings settings, InternalClient client, XPackLicenseState licenseState) {
super(settings);
this.client = client;
this.isTribeNode = settings.getGroups("tribe", true).isEmpty() == false;
this.securityClient = new SecurityClient(client);
this.licenseState = licenseState;
}
public boolean canStart(ClusterState clusterState, boolean master) {
@ -148,7 +155,6 @@ public class NativeRolesStore extends AbstractComponent implements ClusterStateL
public void start() {
try {
if (state.compareAndSet(State.INITIALIZED, State.STARTING)) {
this.securityClient = new SecurityClient(client);
state.set(State.STARTED);
}
} catch (Exception e) {
@ -192,7 +198,8 @@ public class NativeRolesStore extends AbstractComponent implements ClusterStateL
.setFetchSource(true)
.request();
request.indicesOptions().ignoreUnavailable();
InternalClient.fetchAllByEntity(client, request, listener, (hit) -> transformRole(hit.getId(), hit.getSourceRef(), logger));
InternalClient.fetchAllByEntity(client, request, listener,
(hit) -> transformRole(hit.getId(), hit.getSourceRef(), logger, licenseState));
} catch (Exception e) {
logger.error((Supplier<?>) () -> new ParameterizedMessage("unable to retrieve roles {}", Arrays.toString(names)), e);
listener.onFailure(e);
@ -244,16 +251,23 @@ public class NativeRolesStore extends AbstractComponent implements ClusterStateL
listener.onResponse(false);
} else if (isTribeNode) {
listener.onFailure(new UnsupportedOperationException("roles may not be created or modified using a tribe node"));
return;
} else if (canWrite == false) {
} else if (canWrite == false) {
listener.onFailure(new IllegalStateException("role cannot be created or modified as service cannot write until template and " +
"mappings are up to date"));
return;
} else if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) {
innerPutRole(request, role, listener);
} else if (role.isUsingDocumentOrFieldLevelSecurity()) {
listener.onFailure(LicenseUtils.newComplianceException("field and document level security"));
} else {
innerPutRole(request, role, listener);
}
}
// pkg-private for testing
void innerPutRole(final PutRoleRequest request, final RoleDescriptor role, final ActionListener<Boolean> listener) {
try {
client.prepareIndex(SecurityTemplateService.SECURITY_INDEX_NAME, ROLE_DOC_TYPE, role.getName())
.setSource(role.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS))
.setSource(role.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS, false))
.setRefreshPolicy(request.getRefreshPolicy())
.execute(new ActionListener<IndexResponse>() {
@Override
@ -434,15 +448,41 @@ public class NativeRolesStore extends AbstractComponent implements ClusterStateL
if (response.isExists() == false) {
return null;
}
return transformRole(response.getId(), response.getSourceAsBytesRef(), logger);
return transformRole(response.getId(), response.getSourceAsBytesRef(), logger, licenseState);
}
@Nullable
static RoleDescriptor transformRole(String name, BytesReference sourceBytes, Logger logger) {
static RoleDescriptor transformRole(String name, BytesReference sourceBytes, Logger logger, XPackLicenseState licenseState) {
try {
// we pass true as last parameter because we do not want to reject permissions if the field permissions
// are given in 2.x syntax
return RoleDescriptor.parse(name, sourceBytes, true);
RoleDescriptor roleDescriptor = RoleDescriptor.parse(name, sourceBytes, true);
if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) {
return roleDescriptor;
} else {
final boolean dlsEnabled =
Arrays.stream(roleDescriptor.getIndicesPrivileges()).anyMatch(IndicesPrivileges::isUsingDocumentLevelSecurity);
final boolean flsEnabled =
Arrays.stream(roleDescriptor.getIndicesPrivileges()).anyMatch(IndicesPrivileges::isUsingFieldLevelSecurity);
if (dlsEnabled || flsEnabled) {
List<String> unlicensedFeatures = new ArrayList<>(2);
if (flsEnabled) {
unlicensedFeatures.add("fls");
}
if (dlsEnabled) {
unlicensedFeatures.add("dls");
}
Map<String, Object> transientMap = new HashMap<>(2);
transientMap.put("unlicensed_features", unlicensedFeatures);
transientMap.put("enabled", false);
return new RoleDescriptor(roleDescriptor.getName(), roleDescriptor.getClusterPrivileges(),
roleDescriptor.getIndicesPrivileges(), roleDescriptor.getRunAs(), roleDescriptor.getMetadata(), transientMap);
} else {
return roleDescriptor;
}
}
} catch (Exception e) {
logger.error((Supplier<?>) () -> new ParameterizedMessage("error in the format of data for role [{}]", name), e);
return null;

View File

@ -170,7 +170,8 @@ public class AuthorizationServiceTests extends ESTestCase {
if (roleDescriptors.isEmpty()) {
callback.onResponse(Role.EMPTY);
} else {
callback.onResponse(CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache));
callback.onResponse(
CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache));
}
return Void.TYPE;
}).when(rolesStore).roles(any(Set.class), any(FieldPermissionsCache.class), any(ActionListener.class));

View File

@ -54,8 +54,8 @@ public class AuthorizedIndicesTests extends ESTestCase {
.putAlias(new AliasMetaData.Builder("ba").build())
.build(), true)
.build();
Role roles =
CompositeRolesStore.buildRoleFromDescriptors(Sets.newHashSet(aStarRole, bRole), new FieldPermissionsCache(Settings.EMPTY));
Role roles = CompositeRolesStore.buildRoleFromDescriptors(Sets.newHashSet(aStarRole, bRole),
new FieldPermissionsCache(Settings.EMPTY));
AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, roles, SearchAction.NAME, metaData);
List<String> list = authorizedIndices.get();
assertThat(list, containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab"));

View File

@ -136,7 +136,8 @@ public class IndicesAndAliasesResolverTests extends ESTestCase {
if (roleDescriptors.isEmpty()) {
callback.onResponse(Role.EMPTY);
} else {
callback.onResponse(CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache));
callback.onResponse(
CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache));
}
return Void.TYPE;
}).when(rolesStore).roles(any(Set.class), any(FieldPermissionsCache.class), any(ActionListener.class));

View File

@ -17,6 +17,7 @@ import org.elasticsearch.xpack.security.support.MetadataUtils;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authz.permission.FieldPermissions;
import java.util.Collections;
import java.util.Map;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
@ -143,4 +144,15 @@ public class RoleDescriptorTests extends ESTestCase {
assertArrayEquals(new String[] { "m", "n" }, rd.getRunAs());
assertNull(rd.getIndicesPrivileges()[0].getQuery());
}
public void testParseIgnoresTransientMetadata() throws Exception {
final RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all" }, null, null,
Collections.singletonMap("_unlicensed_feature", true), Collections.singletonMap("foo", "bar"));
XContentBuilder b = jsonBuilder();
descriptor.toXContent(b, ToXContent.EMPTY_PARAMS);
RoleDescriptor parsed = RoleDescriptor.parse("test", b.bytes(), false);
assertNotNull(parsed);
assertEquals(1, parsed.getTransientMetadata().size());
assertEquals(true, parsed.getTransientMetadata().get("enabled"));
}
}

View File

@ -9,10 +9,13 @@ import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges;
import org.elasticsearch.xpack.security.authz.permission.Role;
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.security.authz.permission.FieldPermissionsCache;
import org.elasticsearch.xpack.security.authz.permission.Role;
import java.util.Collections;
import java.util.Set;
@ -31,6 +34,124 @@ import static org.mockito.Mockito.when;
public class CompositeRolesStoreTests extends ESTestCase {
public void testRolesWhenDlsFlsUnlicensed() {
XPackLicenseState licenseState = mock(XPackLicenseState.class);
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(false);
RoleDescriptor flsRole = new RoleDescriptor("fls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.grantedFields("*")
.deniedFields("foo")
.indices("*")
.privileges("read")
.build()
}, null);
RoleDescriptor dlsRole = new RoleDescriptor("dls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.indices("*")
.privileges("read")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build()
}, null);
RoleDescriptor flsDlsRole = new RoleDescriptor("fls_dls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.indices("*")
.privileges("read")
.grantedFields("*")
.deniedFields("foo")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build()
}, null);
RoleDescriptor noFlsDlsRole = new RoleDescriptor("no_fls_dls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.indices("*")
.privileges("read")
.build()
}, null);
FileRolesStore fileRolesStore = mock(FileRolesStore.class);
when(fileRolesStore.roleDescriptors(Collections.singleton("fls"))).thenReturn(Collections.singleton(flsRole));
when(fileRolesStore.roleDescriptors(Collections.singleton("dls"))).thenReturn(Collections.singleton(dlsRole));
when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole));
when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole));
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, mock(NativeRolesStore.class),
mock(ReservedRolesStore.class), licenseState);
FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
PlainActionFuture<Role> roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("fls"), fieldPermissionsCache, roleFuture);
assertEquals(Role.EMPTY, roleFuture.actionGet());
roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("dls"), fieldPermissionsCache, roleFuture);
assertEquals(Role.EMPTY, roleFuture.actionGet());
roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("fls_dls"), fieldPermissionsCache, roleFuture);
assertEquals(Role.EMPTY, roleFuture.actionGet());
roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("no_fls_dls"), fieldPermissionsCache, roleFuture);
assertNotEquals(Role.EMPTY, roleFuture.actionGet());
}
public void testRolesWhenDlsFlsLicensed() {
XPackLicenseState licenseState = mock(XPackLicenseState.class);
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true);
RoleDescriptor flsRole = new RoleDescriptor("fls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.grantedFields("*")
.deniedFields("foo")
.indices("*")
.privileges("read")
.build()
}, null);
RoleDescriptor dlsRole = new RoleDescriptor("dls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.indices("*")
.privileges("read")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build()
}, null);
RoleDescriptor flsDlsRole = new RoleDescriptor("fls_dls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.indices("*")
.privileges("read")
.grantedFields("*")
.deniedFields("foo")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build()
}, null);
RoleDescriptor noFlsDlsRole = new RoleDescriptor("no_fls_dls", null, new IndicesPrivileges[] {
IndicesPrivileges.builder()
.indices("*")
.privileges("read")
.build()
}, null);
FileRolesStore fileRolesStore = mock(FileRolesStore.class);
when(fileRolesStore.roleDescriptors(Collections.singleton("fls"))).thenReturn(Collections.singleton(flsRole));
when(fileRolesStore.roleDescriptors(Collections.singleton("dls"))).thenReturn(Collections.singleton(dlsRole));
when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole));
when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole));
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, mock(NativeRolesStore.class),
mock(ReservedRolesStore.class), licenseState);
FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
PlainActionFuture<Role> roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("fls"), fieldPermissionsCache, roleFuture);
assertNotEquals(Role.EMPTY, roleFuture.actionGet());
roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("dls"), fieldPermissionsCache, roleFuture);
assertNotEquals(Role.EMPTY, roleFuture.actionGet());
roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("fls_dls"), fieldPermissionsCache, roleFuture);
assertNotEquals(Role.EMPTY, roleFuture.actionGet());
roleFuture = new PlainActionFuture<>();
compositeRolesStore.roles(Collections.singleton("no_fls_dls"), fieldPermissionsCache, roleFuture);
assertNotEquals(Role.EMPTY, roleFuture.actionGet());
}
public void testNegativeLookupsAreCached() {
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
@ -43,7 +164,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
final CompositeRolesStore compositeRolesStore =
new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore);
new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, new XPackLicenseState());
verify(fileRolesStore).addListener(any(Runnable.class)); // adds a listener in ctor
final String roleName = randomAsciiOfLengthBetween(1, 10);

View File

@ -11,6 +11,7 @@ import org.apache.lucene.util.automaton.MinimizationOperations;
import org.apache.lucene.util.automaton.Operations;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.TestThreadPool;
import org.elasticsearch.threadpool.ThreadPool;
@ -53,6 +54,7 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FileRolesStoreTests extends ESTestCase {
@ -60,7 +62,7 @@ public class FileRolesStoreTests extends ESTestCase {
Path path = getDataPath("roles.yml");
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.builder()
.put(XPackSettings.DLS_FLS_ENABLED.getKey(), true)
.build());
.build(), new XPackLicenseState());
assertThat(roles, notNullValue());
assertThat(roles.size(), is(9));
@ -237,7 +239,7 @@ public class FileRolesStoreTests extends ESTestCase {
Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.builder()
.put(XPackSettings.DLS_FLS_ENABLED.getKey(), false)
.build());
.build(), new XPackLicenseState());
assertThat(roles, notNullValue());
assertThat(roles.size(), is(6));
assertThat(roles.get("role_fields"), nullValue());
@ -258,13 +260,37 @@ public class FileRolesStoreTests extends ESTestCase {
"]. document and field level security is not enabled."));
}
public void testParseFileWithFLSAndDLSUnlicensed() throws Exception {
Path path = getDataPath("roles.yml");
Logger logger = CapturingLogger.newCapturingLogger(Level.WARN);
XPackLicenseState licenseState = mock(XPackLicenseState.class);
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(false);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY, licenseState);
assertThat(roles, notNullValue());
assertThat(roles.size(), is(9));
assertNotNull(roles.get("role_fields"));
assertNotNull(roles.get("role_query"));
assertNotNull(roles.get("role_query_fields"));
List<String> events = CapturingLogger.output(logger.getName(), Level.WARN);
assertThat(events, hasSize(3));
assertThat(
events.get(0),
startsWith("role [role_fields] uses document and/or field level security, which is not enabled by the current license"));
assertThat(events.get(1),
startsWith("role [role_query] uses document and/or field level security, which is not enabled by the current license"));
assertThat(events.get(2),
startsWith("role [role_query_fields] uses document and/or field level security, which is not enabled by the current " +
"license"));
}
/**
* This test is mainly to make sure we can read the default roles.yml config
*/
public void testDefaultRolesFile() throws Exception {
// TODO we should add the config dir to the resources so we don't copy this stuff around...
Path path = getDataPath("default_roles.yml");
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY, new XPackLicenseState());
assertThat(roles, notNullValue());
assertThat(roles.size(), is(0));
}
@ -290,7 +316,7 @@ public class FileRolesStoreTests extends ESTestCase {
threadPool = new TestThreadPool("test");
watcherService = new ResourceWatcherService(settings, threadPool);
final CountDownLatch latch = new CountDownLatch(1);
FileRolesStore store = new FileRolesStore(settings, env, watcherService, latch::countDown);
FileRolesStore store = new FileRolesStore(settings, env, watcherService, latch::countDown, new XPackLicenseState());
Set<RoleDescriptor> descriptors = store.roleDescriptors(Collections.singleton("role1"));
assertThat(descriptors, notNullValue());
@ -334,14 +360,14 @@ public class FileRolesStoreTests extends ESTestCase {
public void testThatEmptyFileDoesNotResultInLoop() throws Exception {
Path file = createTempFile();
Files.write(file, Collections.singletonList("#"), StandardCharsets.UTF_8);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(file, logger, Settings.EMPTY);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(file, logger, Settings.EMPTY, new XPackLicenseState());
assertThat(roles.keySet(), is(empty()));
}
public void testThatInvalidRoleDefinitions() throws Exception {
Path path = getDataPath("invalid_roles.yml");
Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY, new XPackLicenseState());
assertThat(roles.size(), is(1));
assertThat(roles, hasKey("valid_role"));
RoleDescriptor descriptor = roles.get("valid_role");
@ -383,7 +409,7 @@ public class FileRolesStoreTests extends ESTestCase {
Logger logger = CapturingLogger.newCapturingLogger(Level.INFO);
Path path = getDataPath("reserved_roles.yml");
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY);
Map<String, RoleDescriptor> roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY, new XPackLicenseState());
assertThat(roles, notNullValue());
assertThat(roles.size(), is(1));
@ -415,7 +441,7 @@ public class FileRolesStoreTests extends ESTestCase {
.put(XPackSettings.DLS_FLS_ENABLED.getKey(), flsDlsEnabled)
.build();
Environment env = new Environment(settings);
FileRolesStore store = new FileRolesStore(settings, env, mock(ResourceWatcherService.class));
FileRolesStore store = new FileRolesStore(settings, env, mock(ResourceWatcherService.class), new XPackLicenseState());
Map<String, Object> usageStats = store.usageStats();
@ -429,8 +455,7 @@ public class FileRolesStoreTests extends ESTestCase {
Path path = getDataPath("roles2xformat.yml");
byte[] bytes = Files.readAllBytes(path);
String roleString = new String(bytes, Charset.defaultCharset());
RoleDescriptor role = FileRolesStore.parseRoleDescriptor(roleString, path, logger, true,
Settings.EMPTY);
RoleDescriptor role = FileRolesStore.parseRoleDescriptor(roleString, path, logger, true, Settings.EMPTY);
RoleDescriptor.IndicesPrivileges indicesPrivileges = role.getIndicesPrivileges()[0];
assertThat(indicesPrivileges.getGrantedFields(), arrayContaining("foo", "boo"));
assertNull(indicesPrivileges.getDeniedFields());

View File

@ -5,15 +5,55 @@
*/
package org.elasticsearch.xpack.security.authz.store;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.cluster.metadata.IndexTemplateMetaData;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.cluster.routing.IndexRoutingTable;
import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
import org.elasticsearch.cluster.routing.RoutingTable;
import org.elasticsearch.cluster.routing.ShardRouting;
import org.elasticsearch.cluster.routing.UnassignedInfo;
import org.elasticsearch.cluster.routing.UnassignedInfo.Reason;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.ImmutableOpenMap;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityTemplateService;
import org.elasticsearch.xpack.security.action.role.PutRoleRequest;
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.elasticsearch.cluster.routing.RecoverySource.StoreRecoverySource.EXISTING_STORE_INSTANCE;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.hamcrest.Matchers.arrayContaining;
public class NativeRolesStoreTests extends ESTestCase {
@ -23,11 +63,198 @@ public class NativeRolesStoreTests extends ESTestCase {
Path path = getDataPath("roles2xformat.json");
byte[] bytes = Files.readAllBytes(path);
String roleString = new String(bytes, Charset.defaultCharset());
RoleDescriptor role = NativeRolesStore.transformRole("role1", new BytesArray(roleString), logger);
RoleDescriptor role = NativeRolesStore.transformRole("role1", new BytesArray(roleString), logger, new XPackLicenseState());
assertNotNull(role);
assertNotNull(role.getIndicesPrivileges());
RoleDescriptor.IndicesPrivileges indicesPrivileges = role.getIndicesPrivileges()[0];
assertThat(indicesPrivileges.getGrantedFields(), arrayContaining("foo", "boo"));
assertNull(indicesPrivileges.getDeniedFields());
}
public void testRoleDescriptorWithFlsDlsLicensing() throws IOException {
XPackLicenseState licenseState = mock(XPackLicenseState.class);
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(false);
RoleDescriptor flsRole = new RoleDescriptor("fls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().privileges("READ").indices("*")
.grantedFields("*")
.deniedFields("foo")
.build() },
null);
assertFalse(flsRole.getTransientMetadata().containsKey("unlicensed_features"));
RoleDescriptor dlsRole = new RoleDescriptor("dls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("READ")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build() },
null);
assertFalse(dlsRole.getTransientMetadata().containsKey("unlicensed_features"));
RoleDescriptor flsDlsRole = new RoleDescriptor("fls_ dls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("READ")
.grantedFields("*")
.deniedFields("foo")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build() },
null);
assertFalse(flsDlsRole.getTransientMetadata().containsKey("unlicensed_features"));
RoleDescriptor noFlsDlsRole = new RoleDescriptor("no_fls_dls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("READ").build() },
null);
assertFalse(noFlsDlsRole.getTransientMetadata().containsKey("unlicensed_features"));
XContentBuilder builder = flsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
BytesReference bytes = builder.bytes();
RoleDescriptor role = NativeRolesStore.transformRole("fls", bytes, logger, licenseState);
assertNotNull(role);
assertTrue(role.getTransientMetadata().containsKey("unlicensed_features"));
assertThat(role.getTransientMetadata().get("unlicensed_features"), instanceOf(List.class));
assertThat((List<String>) role.getTransientMetadata().get("unlicensed_features"), contains("fls"));
builder = dlsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
role = NativeRolesStore.transformRole("dls", bytes, logger, licenseState);
assertNotNull(role);
assertTrue(role.getTransientMetadata().containsKey("unlicensed_features"));
assertThat(role.getTransientMetadata().get("unlicensed_features"), instanceOf(List.class));
assertThat((List<String>) role.getTransientMetadata().get("unlicensed_features"), contains("dls"));
builder = flsDlsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
role = NativeRolesStore.transformRole("fls_dls", bytes, logger, licenseState);
assertNotNull(role);
assertTrue(role.getTransientMetadata().containsKey("unlicensed_features"));
assertThat(role.getTransientMetadata().get("unlicensed_features"), instanceOf(List.class));
assertThat((List<String>) role.getTransientMetadata().get("unlicensed_features"), contains("fls", "dls"));
builder = noFlsDlsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
role = NativeRolesStore.transformRole("no_fls_dls", bytes, logger, licenseState);
assertNotNull(role);
assertFalse(role.getTransientMetadata().containsKey("unlicensed_features"));
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true);
builder = flsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
role = NativeRolesStore.transformRole("fls", bytes, logger, licenseState);
assertNotNull(role);
assertFalse(role.getTransientMetadata().containsKey("unlicensed_features"));
builder = dlsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
role = NativeRolesStore.transformRole("dls", bytes, logger, licenseState);
assertNotNull(role);
assertFalse(role.getTransientMetadata().containsKey("unlicensed_features"));
builder = flsDlsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
role = NativeRolesStore.transformRole("fls_dls", bytes, logger, licenseState);
assertNotNull(role);
assertFalse(role.getTransientMetadata().containsKey("unlicensed_features"));
builder = noFlsDlsRole.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
role = NativeRolesStore.transformRole("no_fls_dls", bytes, logger, licenseState);
assertNotNull(role);
assertFalse(role.getTransientMetadata().containsKey("unlicensed_features"));
}
public void testPutOfRoleWithFlsDlsUnlicensed() {
final InternalClient internalClient = mock(InternalClient.class);
final XPackLicenseState licenseState = mock(XPackLicenseState.class);
final AtomicBoolean methodCalled = new AtomicBoolean(false);
final NativeRolesStore rolesStore = new NativeRolesStore(Settings.EMPTY, internalClient, licenseState) {
@Override
public State state() {
return State.STARTED;
}
@Override
void innerPutRole(final PutRoleRequest request, final RoleDescriptor role, final ActionListener<Boolean> listener) {
if (methodCalled.compareAndSet(false, true)) {
listener.onResponse(true);
} else {
fail("method called more than once!");
}
}
};
// setup the roles store so the security index exists
rolesStore.clusterChanged(new ClusterChangedEvent("fls_dls_license", getClusterStateWithSecurityIndex(), getEmptyClusterState()));
PutRoleRequest putRoleRequest = new PutRoleRequest();
RoleDescriptor flsRole = new RoleDescriptor("fls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().privileges("READ").indices("*")
.grantedFields("*")
.deniedFields("foo")
.build() },
null);
PlainActionFuture<Boolean> future = new PlainActionFuture<>();
rolesStore.putRole(putRoleRequest, flsRole, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future::actionGet);
assertThat(e.getMessage(), containsString("field and document level security"));
RoleDescriptor dlsRole = new RoleDescriptor("dls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("READ")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build() },
null);
future = new PlainActionFuture<>();
rolesStore.putRole(putRoleRequest, dlsRole, future);
e = expectThrows(ElasticsearchSecurityException.class, future::actionGet);
assertThat(e.getMessage(), containsString("field and document level security"));
RoleDescriptor flsDlsRole = new RoleDescriptor("fls_ dls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("READ")
.grantedFields("*")
.deniedFields("foo")
.query(QueryBuilders.matchAllQuery().buildAsBytes())
.build() },
null);
future = new PlainActionFuture<>();
rolesStore.putRole(putRoleRequest, flsDlsRole, future);
e = expectThrows(ElasticsearchSecurityException.class, future::actionGet);
assertThat(e.getMessage(), containsString("field and document level security"));
RoleDescriptor noFlsDlsRole = new RoleDescriptor("no_fls_dls", null,
new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("READ").build() },
null);
future = new PlainActionFuture<>();
rolesStore.putRole(putRoleRequest, noFlsDlsRole, future);
assertTrue(future.actionGet());
}
private ClusterState getClusterStateWithSecurityIndex() {
Settings settings = Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
.build();
MetaData metaData = MetaData.builder()
.put(IndexMetaData.builder(SecurityTemplateService.SECURITY_INDEX_NAME).settings(settings))
.put(new IndexTemplateMetaData(SecurityTemplateService.SECURITY_TEMPLATE_NAME, 0, 0,
Collections.singletonList(SecurityTemplateService.SECURITY_INDEX_NAME), Settings.EMPTY, ImmutableOpenMap.of(),
ImmutableOpenMap.of(), ImmutableOpenMap.of()))
.build();
Index index = new Index(SecurityTemplateService.SECURITY_INDEX_NAME, UUID.randomUUID().toString());
ShardRouting shardRouting = ShardRouting.newUnassigned(new ShardId(index, 0), true, EXISTING_STORE_INSTANCE,
new UnassignedInfo(Reason.INDEX_CREATED, ""));
IndexShardRoutingTable table = new IndexShardRoutingTable.Builder(new ShardId(index, 0))
.addShard(shardRouting.initialize(randomAsciiOfLength(8), null, shardRouting.getExpectedShardSize()).moveToStarted())
.build();
RoutingTable routingTable = RoutingTable.builder()
.add(IndexRoutingTable
.builder(index)
.addIndexShard(table)
.build())
.build();
return ClusterState.builder(new ClusterName(NativeRolesStoreTests.class.getName()))
.metaData(metaData)
.routingTable(routingTable)
.build();
}
private ClusterState getEmptyClusterState() {
return ClusterState.builder(new ClusterName(NativeRolesStoreTests.class.getName())).build();
}
}