Browse Source

Implement hidden aliases (#52547)

This commit introduces hidden aliases. These are similar to hidden
indices, in that they are not visible by default, unless explicitly
specified by name or by indicating that hidden indices/aliases are
desired.

The new alias property, `is_hidden` is implemented similarly to
`is_write_index`, except that it must be consistent across all indices
with a given alias - that is, all indices with a given alias must
specify the alias as either hidden, or all specify it as non-hidden,
either explicitly or by omitting the `is_hidden` property.
Gordon Brown 5 years ago
parent
commit
351c2f9d40
21 changed files with 838 additions and 58 deletions
  1. 7 0
      docs/reference/indices/aliases.asciidoc
  2. 32 0
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/Alias.java
  3. 24 0
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java
  4. 1 1
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java
  5. 3 3
      server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java
  6. 11 3
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasAction.java
  7. 45 8
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java
  8. 41 1
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasOrIndex.java
  9. 14 12
      server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java
  10. 1 1
      server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java
  11. 2 1
      server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java
  12. 3 0
      server/src/test/java/org/elasticsearch/action/admin/indices/alias/AliasActionsTests.java
  13. 120 3
      server/src/test/java/org/elasticsearch/aliases/IndexAliasesIT.java
  14. 6 0
      server/src/test/java/org/elasticsearch/cluster/metadata/AliasMetaDataTests.java
  15. 132 0
      server/src/test/java/org/elasticsearch/cluster/metadata/AliasOrIndexTests.java
  16. 167 0
      server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java
  17. 121 22
      server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java
  18. 33 0
      server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java
  19. 3 0
      test/framework/src/main/java/org/elasticsearch/index/alias/RandomAliasActionsGenerator.java
  20. 8 2
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java
  21. 64 1
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java

+ 7 - 0
docs/reference/indices/aliases.asciidoc

@@ -106,6 +106,13 @@ include::{docdir}/rest-api/common-parms.asciidoc[tag=index-alias-filter]
 +
 See <<filtered>> for an example.
 
+`is_hidden`::
+(Optional, boolean)
+If `true`, the alias will be excluded from wildcard expressions by default,
+unless overriden in the request using the `expand_wildcards` parameter,
+similar to <<index-hidden,hidden indices>>. This property must be set to the
+same value on all indices that share an alias. Defaults to `false`.
+
 `is_write_index`::
 (Optional, boolean)
 If `true`, assigns the index as an alias's write index.

+ 32 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/alias/Alias.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.action.admin.indices.alias;
 
 import org.elasticsearch.ElasticsearchGenerationException;
+import org.elasticsearch.Version;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
@@ -49,6 +50,7 @@ public class Alias implements Writeable, ToXContentFragment {
     private static final ParseField INDEX_ROUTING = new ParseField("index_routing", "indexRouting", "index-routing");
     private static final ParseField SEARCH_ROUTING = new ParseField("search_routing", "searchRouting", "search-routing");
     private static final ParseField IS_WRITE_INDEX = new ParseField("is_write_index");
+    private static final ParseField IS_HIDDEN = new ParseField("is_hidden");
 
     private String name;
 
@@ -64,12 +66,18 @@ public class Alias implements Writeable, ToXContentFragment {
     @Nullable
     private Boolean writeIndex;
 
+    @Nullable
+    private Boolean isHidden;
+
     public Alias(StreamInput in) throws IOException {
         name = in.readString();
         filter = in.readOptionalString();
         indexRouting = in.readOptionalString();
         searchRouting = in.readOptionalString();
         writeIndex = in.readOptionalBoolean();
+        if (in.getVersion().onOrAfter(Version.V_8_0_0)) { // TODO fix for backport of https://github.com/elastic/elasticsearch/pull/52547
+            isHidden = in.readOptionalBoolean();
+        }
     }
 
     public Alias(String name) {
@@ -189,6 +197,21 @@ public class Alias implements Writeable, ToXContentFragment {
         return this;
     }
 
+    /**
+     * @return whether this alias is hidden or not
+     */
+    public Boolean isHidden() {
+        return isHidden;
+    }
+
+    /**
+     * Sets whether this alias is hidden
+     */
+    public Alias isHidden(@Nullable Boolean isHidden) {
+        this.isHidden = isHidden;
+        return this;
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeString(name);
@@ -196,6 +219,9 @@ public class Alias implements Writeable, ToXContentFragment {
         out.writeOptionalString(indexRouting);
         out.writeOptionalString(searchRouting);
         out.writeOptionalBoolean(writeIndex);
+        if (out.getVersion().onOrAfter(Version.V_8_0_0)) { // TODO fix for backport of https://github.com/elastic/elasticsearch/pull/52547
+            out.writeOptionalBoolean(isHidden);
+        }
     }
 
     /**
@@ -228,6 +254,8 @@ public class Alias implements Writeable, ToXContentFragment {
             } else if (token == XContentParser.Token.VALUE_BOOLEAN) {
                 if (IS_WRITE_INDEX.match(currentFieldName, parser.getDeprecationHandler())) {
                     alias.writeIndex(parser.booleanValue());
+                } else if (IS_HIDDEN.match(currentFieldName, parser.getDeprecationHandler())) {
+                    alias.isHidden(parser.booleanValue());
                 }
             }
         }
@@ -257,6 +285,10 @@ public class Alias implements Writeable, ToXContentFragment {
 
         builder.field(IS_WRITE_INDEX.getPreferredName(), writeIndex);
 
+        if (isHidden != null) {
+            builder.field(IS_HIDDEN.getPreferredName(), isHidden);
+        }
+
         builder.endObject();
         return builder;
     }

+ 24 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java

@@ -97,6 +97,7 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
         private static final ParseField INDEX_ROUTING = new ParseField("index_routing", "indexRouting", "index-routing");
         private static final ParseField SEARCH_ROUTING = new ParseField("search_routing", "searchRouting", "search-routing");
         private static final ParseField IS_WRITE_INDEX = new ParseField("is_write_index");
+        private static final ParseField IS_HIDDEN = new ParseField("is_hidden");
 
         private static final ParseField ADD = new ParseField("add");
         private static final ParseField REMOVE = new ParseField("remove");
@@ -193,6 +194,7 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             ADD_PARSER.declareField(AliasActions::indexRouting, XContentParser::text, INDEX_ROUTING, ValueType.INT);
             ADD_PARSER.declareField(AliasActions::searchRouting, XContentParser::text, SEARCH_ROUTING, ValueType.INT);
             ADD_PARSER.declareField(AliasActions::writeIndex, XContentParser::booleanValue, IS_WRITE_INDEX, ValueType.BOOLEAN);
+            ADD_PARSER.declareField(AliasActions::isHidden, XContentParser::booleanValue, IS_HIDDEN, ValueType.BOOLEAN);
         }
         private static final ObjectParser<AliasActions, Void> REMOVE_PARSER = parser(REMOVE.getPreferredName(), AliasActions::remove);
         private static final ObjectParser<AliasActions, Void> REMOVE_INDEX_PARSER = parser(REMOVE_INDEX.getPreferredName(),
@@ -231,6 +233,7 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
         private String indexRouting;
         private String searchRouting;
         private Boolean writeIndex;
+        private Boolean isHidden;
 
         public AliasActions(AliasActions.Type type) {
             this.type = type;
@@ -248,6 +251,9 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             searchRouting = in.readOptionalString();
             indexRouting = in.readOptionalString();
             writeIndex = in.readOptionalBoolean();
+            if (in.getVersion().onOrAfter(Version.V_8_0_0)) { //TODO fix for backport of https://github.com/elastic/elasticsearch/pull/52547
+                isHidden = in.readOptionalBoolean();
+            }
             originalAliases = in.readStringArray();
         }
 
@@ -261,6 +267,9 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             out.writeOptionalString(searchRouting);
             out.writeOptionalString(indexRouting);
             out.writeOptionalBoolean(writeIndex);
+            if (out.getVersion().onOrAfter(Version.V_8_0_0)) { //TODO fix for backport https://github.com/elastic/elasticsearch/pull/52547
+                out.writeOptionalBoolean(isHidden);
+            }
             out.writeStringArray(originalAliases);
         }
 
@@ -434,6 +443,18 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             return writeIndex;
         }
 
+        public AliasActions isHidden(Boolean isHidden) {
+            if (type != AliasActions.Type.ADD) {
+                throw new IllegalArgumentException("[" + IS_HIDDEN.getPreferredName() + "] is unsupported for [" + type + "]");
+            }
+            this.isHidden = isHidden;
+            return this;
+        }
+
+        public Boolean isHidden() {
+            return isHidden;
+        }
+
         @Override
         public String[] aliases() {
             return aliases;
@@ -492,6 +513,9 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             if (null != writeIndex) {
                 builder.field(IS_WRITE_INDEX.getPreferredName(), writeIndex);
             }
+            if (null != isHidden) {
+                builder.field(IS_HIDDEN.getPreferredName(), isHidden);
+            }
             builder.endObject();
             builder.endObject();
             return builder;

+ 1 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java

@@ -126,7 +126,7 @@ public class TransportIndicesAliasesAction extends TransportMasterNodeAction<Ind
                 case ADD:
                     for (String alias : concreteAliases(action, state.metaData(), index.getName())) {
                         finalActions.add(new AliasAction.Add(index.getName(), alias, action.filter(), action.indexRouting(),
-                            action.searchRouting(), action.writeIndex()));
+                            action.searchRouting(), action.writeIndex(), action.isHidden()));
                     }
                     break;
                 case REMOVE:

+ 3 - 3
server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java

@@ -206,11 +206,11 @@ public class TransportRolloverAction extends TransportMasterNodeAction<RolloverR
                                                      boolean explicitWriteIndex) {
         if (explicitWriteIndex) {
             return List.of(
-                new AliasAction.Add(newIndex, request.getAlias(), null, null, null, true),
-                new AliasAction.Add(oldIndex, request.getAlias(), null, null, null, false));
+                new AliasAction.Add(newIndex, request.getAlias(), null, null, null, true, null),
+                new AliasAction.Add(oldIndex, request.getAlias(), null, null, null, false, null));
         } else {
             return List.of(
-                new AliasAction.Add(newIndex, request.getAlias(), null, null, null, null),
+                new AliasAction.Add(newIndex, request.getAlias(), null, null, null, null, null),
                 new AliasAction.Remove(oldIndex, request.getAlias()));
         }
     }

+ 11 - 3
server/src/main/java/org/elasticsearch/cluster/metadata/AliasAction.java

@@ -85,11 +85,13 @@ public abstract class AliasAction {
         @Nullable
         private final Boolean writeIndex;
 
+        @Nullable final Boolean isHidden;
+
         /**
          * Build the operation.
          */
-        public Add(String index, String alias, @Nullable String filter, @Nullable String indexRouting,
-                   @Nullable String searchRouting, @Nullable Boolean writeIndex) {
+        public Add(String index, String alias, @Nullable String filter, @Nullable String indexRouting, @Nullable String searchRouting,
+                   @Nullable Boolean writeIndex, @Nullable Boolean isHidden) {
             super(index);
             if (false == Strings.hasText(alias)) {
                 throw new IllegalArgumentException("[alias] is required");
@@ -99,6 +101,7 @@ public abstract class AliasAction {
             this.indexRouting = indexRouting;
             this.searchRouting = searchRouting;
             this.writeIndex = writeIndex;
+            this.isHidden = isHidden;
         }
 
         /**
@@ -112,6 +115,11 @@ public abstract class AliasAction {
             return writeIndex;
         }
 
+        @Nullable
+        public Boolean isHidden() {
+            return isHidden;
+        }
+
         @Override
         boolean removeIndex() {
             return false;
@@ -122,7 +130,7 @@ public abstract class AliasAction {
             aliasValidator.validate(alias, indexRouting, filter, writeIndex);
 
             AliasMetaData newAliasMd = AliasMetaData.newAliasMetaDataBuilder(alias).filter(filter).indexRouting(indexRouting)
-                    .searchRouting(searchRouting).writeIndex(writeIndex).build();
+                    .searchRouting(searchRouting).writeIndex(writeIndex).isHidden(isHidden).build();
 
             // Check if this alias already exists
             AliasMetaData currentAliasMd = index.getAliases().get(alias);

+ 45 - 8
server/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.cluster.metadata;
 
 import org.elasticsearch.ElasticsearchGenerationException;
+import org.elasticsearch.Version;
 import org.elasticsearch.cluster.AbstractDiffable;
 import org.elasticsearch.cluster.Diff;
 import org.elasticsearch.common.Nullable;
@@ -40,6 +41,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 import static java.util.Collections.emptySet;
@@ -59,7 +61,11 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
     @Nullable
     private final Boolean writeIndex;
 
-    private AliasMetaData(String alias, CompressedXContent filter, String indexRouting, String searchRouting, Boolean writeIndex) {
+    @Nullable
+    private final Boolean isHidden;
+
+    private AliasMetaData(String alias, CompressedXContent filter, String indexRouting, String searchRouting, Boolean writeIndex,
+                          @Nullable Boolean isHidden) {
         this.alias = alias;
         this.filter = filter;
         this.indexRouting = indexRouting;
@@ -70,10 +76,12 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             searchRoutingValues = emptySet();
         }
         this.writeIndex = writeIndex;
+        this.isHidden = isHidden;
     }
 
     private AliasMetaData(AliasMetaData aliasMetaData, String alias) {
-        this(alias, aliasMetaData.filter(), aliasMetaData.indexRouting(), aliasMetaData.searchRouting(), aliasMetaData.writeIndex());
+        this(alias, aliasMetaData.filter(), aliasMetaData.indexRouting(), aliasMetaData.searchRouting(), aliasMetaData.writeIndex(),
+            aliasMetaData.isHidden);
     }
 
     public String alias() {
@@ -120,6 +128,11 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
         return writeIndex;
     }
 
+    @Nullable
+    public Boolean isHidden() {
+        return isHidden;
+    }
+
     public static Builder builder(String alias) {
         return new Builder(alias);
     }
@@ -142,11 +155,12 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
 
         final AliasMetaData that = (AliasMetaData) o;
 
-        if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
-        if (filter != null ? !filter.equals(that.filter) : that.filter != null) return false;
-        if (indexRouting != null ? !indexRouting.equals(that.indexRouting) : that.indexRouting != null) return false;
-        if (searchRouting != null ? !searchRouting.equals(that.searchRouting) : that.searchRouting != null) return false;
-        if (writeIndex != null ? writeIndex != that.writeIndex : that.writeIndex != null) return false;
+        if (Objects.equals(alias, that.alias) == false) return false;
+        if (Objects.equals(filter, that.filter) == false) return false;
+        if (Objects.equals(indexRouting, that.indexRouting) == false) return false;
+        if (Objects.equals(searchRouting, that.searchRouting) == false) return false;
+        if (Objects.equals(writeIndex, that.writeIndex) == false) return false;
+        if (Objects.equals(isHidden, that.isHidden) == false) return false;
 
         return true;
     }
@@ -183,6 +197,10 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             out.writeBoolean(false);
         }
         out.writeOptionalBoolean(writeIndex());
+
+        if (out.getVersion().onOrAfter(Version.V_8_0_0)) { //TODO fix for backport of https://github.com/elastic/elasticsearch/pull/52547
+            out.writeOptionalBoolean(isHidden);
+        }
     }
 
     public AliasMetaData(StreamInput in) throws IOException {
@@ -205,6 +223,12 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             searchRoutingValues = emptySet();
         }
         writeIndex = in.readOptionalBoolean();
+
+        if (in.getVersion().onOrAfter(Version.V_8_0_0)) { //TODO fix for backport of https://github.com/elastic/elasticsearch/pull/52547
+            isHidden = in.readOptionalBoolean();
+        } else {
+            isHidden = null;
+        }
     }
 
     public static Diff<AliasMetaData> readDiffFrom(StreamInput in) throws IOException {
@@ -235,6 +259,8 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
         @Nullable
         private Boolean writeIndex;
 
+        @Nullable
+        private Boolean isHidden;
 
         public Builder(String alias) {
             this.alias = alias;
@@ -292,8 +318,13 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             return this;
         }
 
+        public Builder isHidden(@Nullable Boolean isHidden) {
+            this.isHidden = isHidden;
+            return this;
+        }
+
         public AliasMetaData build() {
-            return new AliasMetaData(alias, filter, indexRouting, searchRouting, writeIndex);
+            return new AliasMetaData(alias, filter, indexRouting, searchRouting, writeIndex, isHidden);
         }
 
         public static void toXContent(AliasMetaData aliasMetaData, XContentBuilder builder, ToXContent.Params params) throws IOException {
@@ -319,6 +350,10 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
                 builder.field("is_write_index", aliasMetaData.writeIndex());
             }
 
+            if (aliasMetaData.isHidden != null) {
+                builder.field("is_hidden", aliasMetaData.isHidden());
+            }
+
             builder.endObject();
         }
 
@@ -358,6 +393,8 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
                 } else if (token == XContentParser.Token.VALUE_BOOLEAN) {
                     if ("is_write_index".equals(currentFieldName)) {
                         builder.writeIndex(parser.booleanValue());
+                    } else if ("is_hidden".equals(currentFieldName)) {
+                        builder.isHidden(parser.booleanValue());
                     }
                 }
             }

+ 41 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/AliasOrIndex.java

@@ -28,8 +28,12 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
+import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_HIDDEN_SETTING;
+
 /**
  * Encapsulates the  {@link IndexMetaData} instances of a concrete index or indices an alias is pointing to.
  */
@@ -46,6 +50,11 @@ public interface AliasOrIndex {
      */
     List<IndexMetaData> getIndices();
 
+    /**
+     * @return whether this alias/index is hidden or not
+     */
+    boolean isHidden();
+
     /**
      * Represents an concrete index and encapsulates its {@link IndexMetaData}
      */
@@ -66,6 +75,11 @@ public interface AliasOrIndex {
         public List<IndexMetaData> getIndices() {
             return Collections.singletonList(concreteIndex);
         }
+
+        @Override
+        public boolean isHidden() {
+            return INDEX_HIDDEN_SETTING.get(concreteIndex.getSettings());
+        }
     }
 
     /**
@@ -76,11 +90,13 @@ public interface AliasOrIndex {
         private final String aliasName;
         private final List<IndexMetaData> referenceIndexMetaDatas;
         private final SetOnce<IndexMetaData> writeIndex = new SetOnce<>();
+        private final boolean isHidden;
 
         public Alias(AliasMetaData aliasMetaData, IndexMetaData indexMetaData) {
             this.aliasName = aliasMetaData.getAlias();
             this.referenceIndexMetaDatas = new ArrayList<>();
             this.referenceIndexMetaDatas.add(indexMetaData);
+            this.isHidden = aliasMetaData.isHidden() == null ? false : aliasMetaData.isHidden();
         }
 
         @Override
@@ -103,6 +119,11 @@ public interface AliasOrIndex {
             return writeIndex.get();
         }
 
+        @Override
+        public boolean isHidden() {
+            return isHidden;
+        }
+
         /**
          * Returns the unique alias metadata per concrete index.
          *
@@ -135,7 +156,8 @@ public interface AliasOrIndex {
             this.referenceIndexMetaDatas.add(indexMetaData);
         }
 
-        public void computeAndValidateWriteIndex() {
+        public void computeAndValidateAliasProperties() {
+            // Validate write indices
             List<IndexMetaData> writeIndices = referenceIndexMetaDatas.stream()
                 .filter(idxMeta -> Boolean.TRUE.equals(idxMeta.getAliases().get(aliasName).writeIndex()))
                 .collect(Collectors.toList());
@@ -153,6 +175,24 @@ public interface AliasOrIndex {
                 throw new IllegalStateException("alias [" + aliasName + "] has more than one write index [" +
                     Strings.collectionToCommaDelimitedString(writeIndicesStrings) + "]");
             }
+
+            // Validate hidden status
+            final Map<Boolean, List<IndexMetaData>> groupedByHiddenStatus = referenceIndexMetaDatas.stream()
+                    .collect(Collectors.groupingBy(idxMeta -> Boolean.TRUE.equals(idxMeta.getAliases().get(aliasName).isHidden())));
+            if (isNonEmpty(groupedByHiddenStatus.get(true)) && isNonEmpty(groupedByHiddenStatus.get(false))) {
+                List<String> hiddenOn = groupedByHiddenStatus.get(true).stream()
+                    .map(idx -> idx.getIndex().getName()).collect(Collectors.toList());
+                List<String> nonHiddenOn = groupedByHiddenStatus.get(false).stream()
+                    .map(idx -> idx.getIndex().getName()).collect(Collectors.toList());
+                throw new IllegalStateException("alias [" + aliasName + "] has is_hidden set to true on indices [" +
+                    Strings.collectionToCommaDelimitedString(hiddenOn) + "] but does not have is_hidden set to true on indices [" +
+                    Strings.collectionToCommaDelimitedString(nonHiddenOn) + "]; alias must have the same is_hidden setting " +
+                    "on all indices");
+            }
+        }
+
+        private boolean isNonEmpty(List<IndexMetaData> idxMetas) {
+            return (Objects.isNull(idxMetas) || idxMetas.isEmpty()) == false;
         }
     }
 }

+ 14 - 12
server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

@@ -719,7 +719,7 @@ public class IndexNameExpressionResolver {
                     // add all the previous ones...
                     result = new HashSet<>(expressions.subList(0, i));
                 }
-                if (!Regex.isSimpleMatchPattern(expression)) {
+                if (Regex.isSimpleMatchPattern(expression) == false) {
                     //TODO why does wildcard resolver throw exceptions regarding non wildcarded expressions? This should not be done here.
                     if (options.ignoreUnavailable() == false) {
                         AliasOrIndex aliasOrIndex = metaData.getAliasAndIndexLookup().get(expression);
@@ -838,27 +838,29 @@ public class IndexNameExpressionResolver {
                                           String expression, boolean includeHidden) {
             Set<String> expand = new HashSet<>();
             for (Map.Entry<String, AliasOrIndex> entry : matches.entrySet()) {
+                String aliasOrIndexName = entry.getKey();
                 AliasOrIndex aliasOrIndex = entry.getValue();
-                if (context.isPreserveAliases() && aliasOrIndex.isAlias()) {
-                    expand.add(entry.getKey());
-                } else {
-                    for (IndexMetaData meta : aliasOrIndex.getIndices()) {
-                        if (excludeState == null || meta.getState() != excludeState) {
-                            if (includeHidden) {
-                                expand.add(meta.getIndex().getName());
-                            } else if (IndexMetaData.INDEX_HIDDEN_SETTING.get(meta.getSettings()) == false) {
-                                expand.add(meta.getIndex().getName());
-                            } else if (meta.getIndex().getName().startsWith(".") &&
-                                expression.startsWith(".") && Regex.isSimpleMatchPattern(expression)) {
+
+                if (aliasOrIndex.isHidden() == false || includeHidden || implicitHiddenMatch(aliasOrIndexName, expression)) {
+                    if (context.isPreserveAliases() && aliasOrIndex.isAlias()) {
+                        expand.add(aliasOrIndexName);
+                    } else {
+                        for (IndexMetaData meta : aliasOrIndex.getIndices()) {
+                            if (excludeState == null || meta.getState() != excludeState) {
                                 expand.add(meta.getIndex().getName());
                             }
                         }
+
                     }
                 }
             }
             return expand;
         }
 
+        private static boolean implicitHiddenMatch(String itemName, String expression) {
+            return itemName.startsWith(".") && expression.startsWith(".") && Regex.isSimpleMatchPattern(expression);
+        }
+
         private boolean isEmptyOrTrivialWildcard(List<String> expressions) {
             return expressions.isEmpty() || (expressions.size() == 1 && (MetaData.ALL.equals(expressions.get(0)) ||
                 Regex.isMatchAllPattern(expressions.get(0))));

+ 1 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java

@@ -1266,7 +1266,7 @@ public class MetaData implements Iterable<IndexMetaData>, Diffable<MetaData>, To
                 }
             }
             aliasAndIndexLookup.values().stream().filter(AliasOrIndex::isAlias)
-                .forEach(alias -> ((AliasOrIndex.Alias) alias).computeAndValidateWriteIndex());
+                .forEach(alias -> ((AliasOrIndex.Alias) alias).computeAndValidateAliasProperties());
             return aliasAndIndexLookup;
         }
 

+ 2 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java

@@ -548,7 +548,8 @@ public class MetaDataCreateIndexService {
                 aliasValidator.validateAliasFilter(alias.name(), alias.filter(), queryShardContext, xContentRegistry);
             }
             AliasMetaData aliasMetaData = AliasMetaData.builder(alias.name()).filter(alias.filter())
-                .indexRouting(alias.indexRouting()).searchRouting(alias.searchRouting()).writeIndex(alias.writeIndex()).build();
+                .indexRouting(alias.indexRouting()).searchRouting(alias.searchRouting()).writeIndex(alias.writeIndex())
+                .isHidden(alias.isHidden()).build();
             resolvedAliases.add(aliasMetaData);
         }
 

+ 3 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/alias/AliasActionsTests.java

@@ -115,6 +115,7 @@ public class AliasActionsTests extends ESTestCase {
         Object searchRouting = randomBoolean() ? randomRouting() : null;
         Object indexRouting = randomBoolean() ? randomBoolean() ? searchRouting : randomRouting() : null;
         boolean writeIndex = randomBoolean();
+        boolean isHidden = randomBoolean();
         XContentBuilder b = XContentBuilder.builder(randomFrom(XContentType.values()).xContent());
         b.startObject();
         {
@@ -144,6 +145,7 @@ public class AliasActionsTests extends ESTestCase {
                     b.field("index_routing", indexRouting);
                 }
                 b.field("is_write_index", writeIndex);
+                b.field("is_hidden", isHidden);
             }
             b.endObject();
         }
@@ -162,6 +164,7 @@ public class AliasActionsTests extends ESTestCase {
             assertEquals(Objects.toString(searchRouting, null), action.searchRouting());
             assertEquals(Objects.toString(indexRouting, null), action.indexRouting());
             assertEquals(writeIndex, action.writeIndex());
+            assertEquals(isHidden, action.isHidden());
         }
     }
 

+ 120 - 3
server/src/test/java/org/elasticsearch/aliases/IndexAliasesIT.java

@@ -27,7 +27,9 @@ import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.delete.DeleteResponse;
 import org.elasticsearch.action.index.IndexResponse;
 import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.AliasMetaData;
 import org.elasticsearch.cluster.metadata.AliasOrIndex;
@@ -52,6 +54,7 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -76,6 +79,7 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitC
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.emptyArray;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.lessThan;
 import static org.hamcrest.Matchers.notNullValue;
@@ -909,7 +913,8 @@ public class IndexAliasesIT extends ESIntegTestCase {
                 .setMapping("field", "type=text")
                 .addAlias(new Alias("alias1"))
                 .addAlias(new Alias("alias2").filter(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("field"))))
-                .addAlias(new Alias("alias3").indexRouting("index").searchRouting("search")));
+                .addAlias(new Alias("alias3").indexRouting("index").searchRouting("search"))
+                .addAlias(new Alias("alias4").isHidden(true)));
 
         checkAliases();
     }
@@ -919,7 +924,8 @@ public class IndexAliasesIT extends ESIntegTestCase {
                 "    \"aliases\" : {\n" +
                 "        \"alias1\" : {},\n" +
                 "        \"alias2\" : {\"filter\" : {\"match_all\": {}}},\n" +
-                "        \"alias3\" : { \"index_routing\" : \"index\", \"search_routing\" : \"search\"}\n" +
+                "        \"alias3\" : { \"index_routing\" : \"index\", \"search_routing\" : \"search\"},\n" +
+                "        \"alias4\" : {\"is_hidden\":  true}\n" +
                 "    }\n" +
                 "}", XContentType.JSON));
 
@@ -932,7 +938,8 @@ public class IndexAliasesIT extends ESIntegTestCase {
                 .setAliases("{\n" +
                         "        \"alias1\" : {},\n" +
                         "        \"alias2\" : {\"filter\" : {\"term\": {\"field\":\"value\"}}},\n" +
-                        "        \"alias3\" : { \"index_routing\" : \"index\", \"search_routing\" : \"search\"}\n" +
+                        "        \"alias3\" : { \"index_routing\" : \"index\", \"search_routing\" : \"search\"},\n" +
+                        "        \"alias4\" : {\"is_hidden\":  true}\n" +
                         "}"));
 
         checkAliases();
@@ -1119,6 +1126,104 @@ public class IndexAliasesIT extends ESIntegTestCase {
         assertHitCount(client().prepareSearch("test").get(), 1);
     }
 
+    public void testHiddenAliasesMustBeConsistent() {
+        final String index1 = randomAlphaOfLength(5).toLowerCase(Locale.ROOT);
+        final String index2 = randomAlphaOfLength(6).toLowerCase(Locale.ROOT);
+        final String alias = randomAlphaOfLength(7).toLowerCase(Locale.ROOT);
+        createIndex(index1, index2);
+
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index1).alias(alias)));
+
+        IllegalStateException ex = expectThrows(IllegalStateException.class, () -> {
+            AcknowledgedResponse res = admin().indices().prepareAliases()
+                .addAliasAction(AliasActions.add().index(index2).alias(alias).isHidden(true)).get();
+        });
+        logger.error("exception: {}", ex.getMessage());
+        assertThat(ex.getMessage(), containsString("has is_hidden set to true on indices"));
+
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.remove().index(index1).alias(alias)));
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index1).alias(alias).isHidden(false)));
+        expectThrows(IllegalStateException.class,
+            () -> admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index2).alias(alias).isHidden(true)).get());
+
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.remove().index(index1).alias(alias)));
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index1).alias(alias).isHidden(true)));
+        expectThrows(IllegalStateException.class,
+            () -> admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index2).alias(alias).isHidden(false)).get());
+        expectThrows(IllegalStateException.class,
+            () -> admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index2).alias(alias)).get());
+
+        // Both visible
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.remove().index(index1).alias(alias)));
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index1).alias(alias).isHidden(false)));
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index2).alias(alias).isHidden(false)));
+
+        // Both hidden
+        assertAcked(admin().indices().prepareAliases()
+            .addAliasAction(AliasActions.remove().index(index1).alias(alias))
+            .addAliasAction(AliasActions.remove().index(index2).alias(alias)));
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index1).alias(alias).isHidden(true)));
+        assertAcked(admin().indices().prepareAliases().addAliasAction(AliasActions.add().index(index2).alias(alias).isHidden(true)));
+
+        // Visible on one, then update it to hidden & add to a second as hidden simultaneously
+        assertAcked(admin().indices().prepareAliases()
+            .addAliasAction(AliasActions.remove().index(index1).alias(alias))
+            .addAliasAction(AliasActions.remove().index(index2).alias(alias)));
+        assertAcked(admin().indices().prepareAliases()
+            .addAliasAction(AliasActions.add().index(index2).alias(alias).isHidden(false)));
+        assertAcked(admin().indices().prepareAliases()
+            .addAliasAction(AliasActions.add().index(index1).alias(alias).isHidden(true))
+            .addAliasAction(AliasActions.add().index(index2).alias(alias).isHidden(true)));
+    }
+
+    public void testIndexingAndQueryingHiddenAliases() throws Exception {
+        final String writeIndex = randomAlphaOfLength(5).toLowerCase(Locale.ROOT);
+        final String nonWriteIndex = randomAlphaOfLength(6).toLowerCase(Locale.ROOT);
+        final String alias = "alias-" + randomAlphaOfLength(7).toLowerCase(Locale.ROOT);
+        createIndex(writeIndex, nonWriteIndex);
+
+        assertAcked(admin().indices().prepareAliases()
+            .addAliasAction(AliasActions.add().index(writeIndex).alias(alias).isHidden(true).writeIndex(true))
+            .addAliasAction(AliasActions.add().index(nonWriteIndex).alias(alias).isHidden(true)));
+
+        ensureGreen();
+
+        // Put a couple docs in each index directly
+        IndexResponse res = client().index(indexRequest(nonWriteIndex).id("1").source(source("1", "nonwrite"), XContentType.JSON)).get();
+        assertThat(res.status().getStatus(), equalTo(201));
+        res = client().index(indexRequest(writeIndex).id("2").source(source("2", "writeindex"), XContentType.JSON)).get();
+        assertThat(res.status().getStatus(), equalTo(201));
+        // And through the alias
+        res = client().index(indexRequest(alias).id("3").source(source("3", "through alias"), XContentType.JSON)).get();
+        assertThat(res.status().getStatus(), equalTo(201));
+
+        refresh(writeIndex, nonWriteIndex);
+
+        // Make sure that the doc written to the alias made it
+        SearchResponse searchResponse = client().prepareSearch(writeIndex).setQuery(QueryBuilders.matchAllQuery()).get();
+        assertHits(searchResponse.getHits(), "2", "3");
+
+        // Ensure that all docs can be gotten through the alias
+        searchResponse = client().prepareSearch(alias).setQuery(QueryBuilders.matchAllQuery()).get();
+        assertHits(searchResponse.getHits(), "1", "2", "3");
+
+        // And querying using a wildcard with indices options set to expand hidden
+        searchResponse = client().prepareSearch("alias*")
+            .setQuery(QueryBuilders.matchAllQuery())
+            .setIndicesOptions(IndicesOptions.fromOptions(false, false, true, false, true, true, true, false, false)).get();
+        assertHits(searchResponse.getHits(), "1", "2", "3");
+
+        // And that querying the alias with a wildcard and no expand options fails
+        searchResponse = client().prepareSearch("alias*").setQuery(QueryBuilders.matchAllQuery()).get();
+        assertThat(searchResponse.getHits().getHits(), emptyArray());
+    }
+
+    public void testGetAliasAndAliasExistsForHiddenAliases() {
+        final String writeIndex = randomAlphaOfLength(5).toLowerCase(Locale.ROOT);
+        final String nonWriteIndex = randomAlphaOfLength(6).toLowerCase(Locale.ROOT);
+        final String alias = "alias-" + randomAlphaOfLength(7).toLowerCase(Locale.ROOT);
+    }
+
     private void checkAliases() {
         GetAliasesResponse getAliasesResponse = admin().indices().prepareGetAliases("alias1").get();
         assertThat(getAliasesResponse.getAliases().get("test").size(), equalTo(1));
@@ -1127,6 +1232,7 @@ public class IndexAliasesIT extends ESIntegTestCase {
         assertThat(aliasMetaData.filter(), nullValue());
         assertThat(aliasMetaData.indexRouting(), nullValue());
         assertThat(aliasMetaData.searchRouting(), nullValue());
+        assertThat(aliasMetaData.isHidden(), nullValue());
 
         getAliasesResponse = admin().indices().prepareGetAliases("alias2").get();
         assertThat(getAliasesResponse.getAliases().get("test").size(), equalTo(1));
@@ -1135,6 +1241,7 @@ public class IndexAliasesIT extends ESIntegTestCase {
         assertThat(aliasMetaData.filter(), notNullValue());
         assertThat(aliasMetaData.indexRouting(), nullValue());
         assertThat(aliasMetaData.searchRouting(), nullValue());
+        assertThat(aliasMetaData.isHidden(), nullValue());
 
         getAliasesResponse = admin().indices().prepareGetAliases("alias3").get();
         assertThat(getAliasesResponse.getAliases().get("test").size(), equalTo(1));
@@ -1143,6 +1250,16 @@ public class IndexAliasesIT extends ESIntegTestCase {
         assertThat(aliasMetaData.filter(), nullValue());
         assertThat(aliasMetaData.indexRouting(), equalTo("index"));
         assertThat(aliasMetaData.searchRouting(), equalTo("search"));
+        assertThat(aliasMetaData.isHidden(), nullValue());
+
+        getAliasesResponse = admin().indices().prepareGetAliases("alias4").get();
+        assertThat(getAliasesResponse.getAliases().get("test").size(), equalTo(1));
+        aliasMetaData = getAliasesResponse.getAliases().get("test").get(0);
+        assertThat(aliasMetaData.alias(), equalTo("alias4"));
+        assertThat(aliasMetaData.filter(), nullValue());
+        assertThat(aliasMetaData.indexRouting(), nullValue());
+        assertThat(aliasMetaData.searchRouting(), nullValue());
+        assertThat(aliasMetaData.isHidden(), equalTo(true));
     }
 
     private void assertHits(SearchHits hits, String... ids) {

+ 6 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/AliasMetaDataTests.java

@@ -42,6 +42,7 @@ public class AliasMetaDataTests extends AbstractXContentTestCase<AliasMetaData>
                         .routing("routing")
                         .searchRouting("trim,tw , ltw , lw")
                         .writeIndex(randomBoolean() ? null : randomBoolean())
+                        .isHidden(randomBoolean() ? null : randomBoolean())
                         .build();
 
         assertThat(before.searchRoutingValues(), equalTo(Sets.newHashSet("trim", "tw ", " ltw ", " lw")));
@@ -64,6 +65,7 @@ public class AliasMetaDataTests extends AbstractXContentTestCase<AliasMetaData>
                 .indexRouting(expectedInstance.indexRouting())
                 .searchRouting(expectedInstance.searchRouting())
                 .writeIndex(randomBoolean() ? null : randomBoolean())
+                .isHidden(randomBoolean() ? null : randomBoolean())
                 .build();
         }
         assertEquals(expectedInstance, newInstance);
@@ -112,6 +114,10 @@ public class AliasMetaDataTests extends AbstractXContentTestCase<AliasMetaData>
             builder.filter("{\"term\":{\"year\":2016}}");
         }
         builder.writeIndex(randomBoolean());
+
+        if (randomBoolean()) {
+            builder.isHidden(randomBoolean());
+        }
         return builder.build();
     }
 

+ 132 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/AliasOrIndexTests.java

@@ -0,0 +1,132 @@
+/*
+ *
+ *  * Licensed to Elasticsearch under one or more contributor
+ *  * license agreements. See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright
+ *  * ownership. Elasticsearch licenses this file to you under
+ *  * the Apache License, Version 2.0 (the "License"); you may
+ *  * not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *    http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing,
+ *  * software distributed under the License is distributed on an
+ *  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  * KIND, either express or implied.  See the License for the
+ *  * specific language governing permissions and limitations
+ *  * under the License.
+ *
+ */
+
+package org.elasticsearch.cluster.metadata;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.test.ESTestCase;
+
+import java.util.Objects;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+
+public class AliasOrIndexTests extends ESTestCase {
+
+    public void testHiddenAliasValidation() {
+        final String hiddenAliasName = "hidden_alias";
+        AliasMetaData hiddenAliasMetadata = new AliasMetaData.Builder(hiddenAliasName).isHidden(true).build();
+
+        IndexMetaData hidden1 = buildIndexWithAlias("hidden1", hiddenAliasName, true);
+        IndexMetaData hidden2 = buildIndexWithAlias("hidden2", hiddenAliasName, true);
+        IndexMetaData hidden3 = buildIndexWithAlias("hidden3", hiddenAliasName, true);
+
+        IndexMetaData indexWithNonHiddenAlias = buildIndexWithAlias("nonhidden1", hiddenAliasName, false);
+        IndexMetaData indexWithUnspecifiedAlias = buildIndexWithAlias("nonhidden2", hiddenAliasName, null);
+
+        {
+            AliasOrIndex.Alias allHidden = new AliasOrIndex.Alias(hiddenAliasMetadata, hidden1);
+            allHidden.addIndex(hidden2);
+            allHidden.addIndex(hidden3);
+            allHidden.computeAndValidateAliasProperties(); // Should be ok
+        }
+
+        {
+            AliasOrIndex.Alias allVisible;
+            if (randomBoolean()) {
+                allVisible = new AliasOrIndex.Alias(hiddenAliasMetadata, indexWithNonHiddenAlias);
+                allVisible.addIndex(indexWithUnspecifiedAlias);
+            } else {
+                allVisible = new AliasOrIndex.Alias(hiddenAliasMetadata, indexWithUnspecifiedAlias);
+                allVisible.addIndex(indexWithNonHiddenAlias);
+            }
+
+            allVisible.computeAndValidateAliasProperties(); // Should be ok
+        }
+
+        {
+            AliasOrIndex.Alias oneNonHidden = new AliasOrIndex.Alias(hiddenAliasMetadata, hidden1);
+            oneNonHidden.addIndex(hidden2);
+            oneNonHidden.addIndex(hidden3);
+            oneNonHidden.addIndex(indexWithNonHiddenAlias);
+            IllegalStateException exception = expectThrows(IllegalStateException.class,
+                () -> oneNonHidden.computeAndValidateAliasProperties());
+            assertThat(exception.getMessage(), containsString("alias [" + hiddenAliasName +
+                "] has is_hidden set to true on indices ["));
+            assertThat(exception.getMessage(), allOf(containsString(hidden1.getIndex().getName()),
+                containsString(hidden2.getIndex().getName()),
+                containsString(hidden3.getIndex().getName())));
+            assertThat(exception.getMessage(), containsString("but does not have is_hidden set to true on indices [" +
+                indexWithNonHiddenAlias.getIndex().getName() + "]; alias must have the same is_hidden setting on all indices"));
+        }
+
+        {
+            AliasOrIndex.Alias oneUnspecified = new AliasOrIndex.Alias(hiddenAliasMetadata, hidden1);
+            oneUnspecified.addIndex(hidden2);
+            oneUnspecified.addIndex(hidden3);
+            oneUnspecified.addIndex(indexWithUnspecifiedAlias);
+            IllegalStateException exception = expectThrows(IllegalStateException.class,
+                () -> oneUnspecified.computeAndValidateAliasProperties());
+            assertThat(exception.getMessage(), containsString("alias [" + hiddenAliasName +
+                "] has is_hidden set to true on indices ["));
+            assertThat(exception.getMessage(), allOf(containsString(hidden1.getIndex().getName()),
+                containsString(hidden2.getIndex().getName()),
+                containsString(hidden3.getIndex().getName())));
+            assertThat(exception.getMessage(), containsString("but does not have is_hidden set to true on indices [" +
+                indexWithUnspecifiedAlias.getIndex().getName() + "]; alias must have the same is_hidden setting on all indices"));
+        }
+
+        {
+            AliasOrIndex.Alias mostlyVisibleOneHidden;
+            if (randomBoolean()) {
+                mostlyVisibleOneHidden = new AliasOrIndex.Alias(hiddenAliasMetadata, indexWithNonHiddenAlias);
+                mostlyVisibleOneHidden.addIndex(indexWithUnspecifiedAlias);
+            } else {
+                mostlyVisibleOneHidden = new AliasOrIndex.Alias(hiddenAliasMetadata, indexWithUnspecifiedAlias);
+                mostlyVisibleOneHidden.addIndex(indexWithNonHiddenAlias);
+            }
+            final IndexMetaData hiddenIndex = randomFrom(hidden1, hidden2, hidden3);
+            mostlyVisibleOneHidden.addIndex(hiddenIndex);
+            IllegalStateException exception = expectThrows(IllegalStateException.class,
+                () -> mostlyVisibleOneHidden.computeAndValidateAliasProperties());
+            assertThat(exception.getMessage(), containsString("alias [" + hiddenAliasName + "] has is_hidden set to true on " +
+                "indices [" + hiddenIndex.getIndex().getName() + "] but does not have is_hidden set to true on indices ["));
+            assertThat(exception.getMessage(), allOf(containsString(indexWithUnspecifiedAlias.getIndex().getName()),
+                containsString(indexWithNonHiddenAlias.getIndex().getName())));
+            assertThat(exception.getMessage(), containsString("but does not have is_hidden set to true on indices ["));
+        }
+    }
+
+    private IndexMetaData buildIndexWithAlias(String indexName, String aliasName, @Nullable Boolean aliasIsHidden) {
+        final AliasMetaData.Builder aliasMetaData = new AliasMetaData.Builder(aliasName);
+        if (Objects.nonNull(aliasIsHidden) || randomBoolean()) {
+            aliasMetaData.isHidden(aliasIsHidden);
+        }
+        return new IndexMetaData.Builder(indexName)
+            .settings(settings(Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .putAlias(aliasMetaData)
+            .build();
+    }
+
+}

+ 167 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java

@@ -49,11 +49,14 @@ import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
 
+import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_HIDDEN_SETTING;
 import static org.elasticsearch.common.util.set.Sets.newHashSet;
 import static org.hamcrest.Matchers.arrayContaining;
 import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
 import static org.hamcrest.Matchers.arrayWithSize;
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.emptyArray;
 import static org.hamcrest.Matchers.endsWith;
 import static org.hamcrest.Matchers.equalTo;
@@ -883,6 +886,170 @@ public class IndexNameExpressionResolverTests extends ESTestCase {
         assertTrue(indexNames.contains("bar_bar"));
     }
 
+    public void testHiddenAliasAndHiddenIndexResolution() {
+        final String visibleIndex = "visible_index";
+        final String hiddenIndex = "hidden_index";
+        final String visibleAlias = "visible_alias";
+        final String hiddenAlias = "hidden_alias";
+        final String dottedHiddenAlias = ".hidden_alias";
+        final String dottedHiddenIndex = ".hidden_index";
+
+        IndicesOptions excludeHiddenOptions = IndicesOptions.fromOptions(false, false, true, false, false, true, false, false, false);
+        IndicesOptions includeHiddenOptions = IndicesOptions.fromOptions(false, false, true, false, true, true, false, false, false);
+
+        {
+            // A visible index with a visible alias and a hidden index with a hidden alias
+            MetaData.Builder mdBuilder = MetaData.builder()
+                .put(indexBuilder(visibleIndex).state(State.OPEN).putAlias(AliasMetaData.builder(visibleAlias)))
+                .put(indexBuilder(hiddenIndex,  Settings.builder().put(INDEX_HIDDEN_SETTING.getKey(), true).build())
+                    .state(State.OPEN)
+                    .putAlias(AliasMetaData.builder(hiddenAlias).isHidden(true)));
+            ClusterState state = ClusterState.builder(new ClusterName("_name")).metaData(mdBuilder).build();
+
+            // A total wildcard should only be resolved to visible indices
+            String[] indexNames;
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex));
+
+            // Unless hidden is specified in the options
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+
+            // Both hidden indices and hidden aliases should not be included in wildcard resolution
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "hidden*", "visible*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex));
+
+            // unless it's specified in the options
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "hidden*", "visible*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+
+            // Only visible aliases should be included in wildcard resolution
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "*_alias");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex));
+
+            // unless, again, it's specified in the options
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "*_alias");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+
+            // If we specify a hidden alias by name, the options shouldn't matter.
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, hiddenAlias);
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(hiddenIndex));
+
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, hiddenAlias);
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(hiddenIndex));
+        }
+
+        {
+            // A visible alias that points to one hidden and one visible index
+            MetaData.Builder mdBuilder = MetaData.builder()
+                .put(indexBuilder(visibleIndex).state(State.OPEN).putAlias(AliasMetaData.builder(visibleAlias)))
+                .put(indexBuilder(hiddenIndex, Settings.builder().put(INDEX_HIDDEN_SETTING.getKey(), true).build())
+                    .state(State.OPEN)
+                    .putAlias(AliasMetaData.builder(visibleAlias)));
+            ClusterState state = ClusterState.builder(new ClusterName("_name")).metaData(mdBuilder).build();
+
+            // If the alias is resolved to concrete indices, it should resolve to all the indices it points to, hidden or not.
+            String[] indexNames;
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "*_alias");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "*_alias");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, visibleAlias);
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, visibleAlias);
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+
+            // A total wildcards does not resolve the hidden index in this case
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex));
+        }
+
+        {
+            // A hidden alias that points to one hidden and one visible index
+            MetaData.Builder mdBuilder = MetaData.builder()
+                .put(indexBuilder(visibleIndex).state(State.OPEN).putAlias(AliasMetaData.builder(hiddenAlias).isHidden(true)))
+                .put(indexBuilder(hiddenIndex, Settings.builder().put(INDEX_HIDDEN_SETTING.getKey(), true).build())
+                    .state(State.OPEN)
+                    .putAlias(AliasMetaData.builder(hiddenAlias).isHidden(true)));
+            ClusterState state = ClusterState.builder(new ClusterName("_name")).metaData(mdBuilder).build();
+
+            String[] indexNames;
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex));
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+
+            // A query that only matches the hidden alias should throw
+            expectThrows(IndexNotFoundException.class,
+                () -> indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "*_alias"));
+
+            // But if we include hidden it should be resolved to both indices
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "*_alias");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+
+            // If we specify the alias by name it should resolve to both indices, regardless of if the options specify hidden
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, hiddenAlias);
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, hiddenAlias);
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(visibleIndex, hiddenIndex));
+        }
+
+        {
+            // A hidden alias with a dot-prefixed name that points to one hidden index with a dot prefix, and one hidden index without
+            MetaData.Builder mdBuilder = MetaData.builder()
+                .put(indexBuilder(dottedHiddenIndex, Settings.builder().put(INDEX_HIDDEN_SETTING.getKey(), true).build())
+                    .state(State.OPEN)
+                    .putAlias(AliasMetaData.builder(dottedHiddenAlias).isHidden(true)))
+                .put(indexBuilder(hiddenIndex, Settings.builder().put(INDEX_HIDDEN_SETTING.getKey(), true).build())
+                    .state(State.OPEN)
+                    .putAlias(AliasMetaData.builder(dottedHiddenAlias).isHidden(true)));
+            ClusterState state = ClusterState.builder(new ClusterName("_name")).metaData(mdBuilder).build();
+
+            String[] indexNames;
+            // A dot-prefixed pattern that includes only the hidden alias should resolve to both, regardless of the options
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, ".hidden_a*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(dottedHiddenIndex, hiddenIndex));
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, ".hidden_a*");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(dottedHiddenIndex, hiddenIndex));
+
+            // A query that doesn't include the dot should fail if the options don't include hidden
+            expectThrows(IndexNotFoundException.class,
+                () -> indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "*_alias"));
+
+            // But should include both indices if the options do include hidden
+            indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "*_alias");
+            assertThat(Arrays.asList(indexNames), containsInAnyOrder(dottedHiddenIndex, hiddenIndex));
+
+        }
+    }
+
+    public void testHiddenIndexWithVisibleAliasOverlappingNameResolution() {
+        final String hiddenIndex = "my-hidden-index";
+        final String hiddenAlias = "my-hidden-alias";
+        final String visibleAlias = "my-visible-alias";
+
+        IndicesOptions excludeHiddenOptions = IndicesOptions.fromOptions(false, true, true, false, false, true, false, false, false);
+        IndicesOptions includeHiddenOptions = IndicesOptions.fromOptions(false, true, true, false, true, true, false, false, false);
+
+        MetaData.Builder mdBuilder = MetaData.builder()
+            .put(indexBuilder(hiddenIndex,  Settings.builder().put(INDEX_HIDDEN_SETTING.getKey(), true).build())
+                .state(State.OPEN)
+                .putAlias(AliasMetaData.builder(hiddenAlias).isHidden(true))
+                .putAlias(AliasMetaData.builder(visibleAlias).build()));
+        ClusterState state = ClusterState.builder(new ClusterName("_name")).metaData(mdBuilder).build();
+
+        String[] indexNames;
+        indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "my-*");
+        assertThat(Arrays.asList(indexNames), containsInAnyOrder(hiddenIndex));
+
+        indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "my-hidden*");
+        assertThat(Arrays.asList(indexNames), empty());
+        indexNames = indexNameExpressionResolver.concreteIndexNames(state, excludeHiddenOptions, "my-*", "-my-visible*");
+        assertThat(Arrays.asList(indexNames), empty());
+        indexNames = indexNameExpressionResolver.concreteIndexNames(state, includeHiddenOptions, "my-hidden*", "-my-hidden-a*");
+        assertThat(Arrays.asList(indexNames), empty());
+    }
+
     /**
      * test resolving _all pattern (null, empty array or "_all") for random IndicesOptions
      */

+ 121 - 22
server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java

@@ -38,6 +38,7 @@ import java.util.List;
 
 import static java.util.Collections.singletonList;
 import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.startsWith;
 import static org.mockito.Matchers.any;
@@ -72,7 +73,8 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), index);
 
         // Add an alias to it
-        ClusterState after = service.applyAliasActions(before, singletonList(new AliasAction.Add(index, "test", null, null, null, null)));
+        ClusterState after = service.applyAliasActions(before, singletonList(new AliasAction.Add(index, "test", null, null, null, null,
+            null)));
         AliasOrIndex alias = after.metaData().getAliasAndIndexLookup().get("test");
         assertNotNull(alias);
         assertTrue(alias.isAlias());
@@ -83,7 +85,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         before = after;
         after = service.applyAliasActions(before, Arrays.asList(
                 new AliasAction.Remove(index, "test"),
-                new AliasAction.Add(index, "test_2", null, null, null, null)));
+                new AliasAction.Add(index, "test_2", null, null, null, null, null)));
         assertNull(after.metaData().getAliasAndIndexLookup().get("test"));
         alias = after.metaData().getAliasAndIndexLookup().get("test_2");
         assertNotNull(alias);
@@ -107,7 +109,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         for (int i = 0; i < length; i++) {
             final String index = randomValueOtherThanMany(v -> indices.add(v) == false, () -> randomAlphaOfLength(8));
             before = createIndex(before, index);
-            addActions.add(new AliasAction.Add(index, "alias-" + index, null, null, null, null));
+            addActions.add(new AliasAction.Add(index, "alias-" + index, null, null, null, null, null));
         }
         final ClusterState afterAddingAliasesToAll = service.applyAliasActions(before, addActions);
         assertAliasesVersionIncreased(indices.toArray(new String[0]), before, afterAddingAliasesToAll);
@@ -117,7 +119,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         final var randomAddActions = new ArrayList<AliasAction>(length);
         for (var index : indices) {
             if (randomBoolean()) {
-                randomAddActions.add(new AliasAction.Add(index, "random-alias-" + index, null, null, null, null));
+                randomAddActions.add(new AliasAction.Add(index, "random-alias-" + index, null, null, null, null, null));
                 randomIndices.add(index);
             }
         }
@@ -134,17 +136,18 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         final ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), index);
 
         final ClusterState afterAddWriteAlias =
-                service.applyAliasActions(before, singletonList(new AliasAction.Add(index, "test", null, null, null, true)));
+                service.applyAliasActions(before, singletonList(new AliasAction.Add(index, "test", null, null, null, true, null)));
         assertAliasesVersionIncreased(index, before, afterAddWriteAlias);
 
         final ClusterState afterChangeWriteAliasToNonWriteAlias =
-                service.applyAliasActions(afterAddWriteAlias, singletonList(new AliasAction.Add(index, "test", null, null, null, false)));
+                service.applyAliasActions(afterAddWriteAlias, singletonList(new AliasAction.Add(index, "test", null, null, null, false,
+                    null)));
         assertAliasesVersionIncreased(index, afterAddWriteAlias, afterChangeWriteAliasToNonWriteAlias);
 
         final ClusterState afterChangeNonWriteAliasToWriteAlias =
                 service.applyAliasActions(
                         afterChangeWriteAliasToNonWriteAlias,
-                        singletonList(new AliasAction.Add(index, "test", null, null, null, true)));
+                        singletonList(new AliasAction.Add(index, "test", null, null, null, true, null)));
         assertAliasesVersionIncreased(index, afterChangeWriteAliasToNonWriteAlias, afterChangeNonWriteAliasToWriteAlias);
     }
 
@@ -156,7 +159,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         final int length = randomIntBetween(2, 8);
         final var addActions = new ArrayList<AliasAction>(length);
         for (int i = 0; i < length; i++) {
-            addActions.add(new AliasAction.Add(index, "test", null, null, null, null));
+            addActions.add(new AliasAction.Add(index, "test", null, null, null, null, null));
         }
         final ClusterState afterAddingAliases = service.applyAliasActions(before, addActions);
 
@@ -173,7 +176,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         final var addActions = new ArrayList<AliasAction>(length);
         for (int i = 0; i < length; i++) {
             final String aliasName = randomValueOtherThanMany(v -> aliasNames.add(v) == false, () -> randomAlphaOfLength(8));
-            addActions.add(new AliasAction.Add(index, aliasName, null, null, null, null));
+            addActions.add(new AliasAction.Add(index, aliasName, null, null, null, null, null));
         }
         final ClusterState afterAddingAlias = service.applyAliasActions(before, addActions);
 
@@ -181,7 +184,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         final var removeAndAddActions = new ArrayList<AliasAction>(2 * length);
         for (final var aliasName : aliasNames) {
             removeAndAddActions.add(new AliasAction.Remove(index, aliasName));
-            removeAndAddActions.add(new AliasAction.Add(index, aliasName, null, null, null, null));
+            removeAndAddActions.add(new AliasAction.Add(index, aliasName, null, null, null, null, null));
         }
         final ClusterState afterRemoveAndAddAlias = service.applyAliasActions(afterAddingAlias, removeAndAddActions);
         assertAliasesVersionUnchanged(index, afterAddingAlias, afterRemoveAndAddAlias);
@@ -194,7 +197,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
 
         // Now remove "test" and add an alias to "test" to "test_2" in one go
         ClusterState after = service.applyAliasActions(before, Arrays.asList(
-                new AliasAction.Add("test_2", "test", null, null, null, null),
+                new AliasAction.Add("test_2", "test", null, null, null, null, null),
                 new AliasAction.RemoveIndex("test")));
         AliasOrIndex alias = after.metaData().getAliasAndIndexLookup().get("test");
         assertNotNull(alias);
@@ -209,7 +212,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
 
         // Attempt to add an alias to "test" at the same time as we remove it
         IndexNotFoundException e = expectThrows(IndexNotFoundException.class, () -> service.applyAliasActions(before, Arrays.asList(
-                new AliasAction.Add("test", "alias", null, null, null, null),
+                new AliasAction.Add("test", "alias", null, null, null, null, null),
                 new AliasAction.RemoveIndex("test"))));
         assertEquals("test", e.getIndex().getName());
     }
@@ -229,20 +232,20 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), "test");
 
         ClusterState after = service.applyAliasActions(before, Arrays.asList(
-            new AliasAction.Add("test", "alias", null, null, null, false)));
+            new AliasAction.Add("test", "alias", null, null, null, false, null)));
         assertFalse(after.metaData().index("test").getAliases().get("alias").writeIndex());
         assertNull(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex());
         assertAliasesVersionIncreased("test", before, after);
 
         after = service.applyAliasActions(before, Arrays.asList(
-            new AliasAction.Add("test", "alias", null, null, null, null)));
+            new AliasAction.Add("test", "alias", null, null, null, null, null)));
         assertNull(after.metaData().index("test").getAliases().get("alias").writeIndex());
         assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
             equalTo(after.metaData().index("test")));
         assertAliasesVersionIncreased("test", before, after);
 
         after = service.applyAliasActions(before, Arrays.asList(
-            new AliasAction.Add("test", "alias", null, null, null, true)));
+            new AliasAction.Add("test", "alias", null, null, null, true, null)));
         assertTrue(after.metaData().index("test").getAliases().get("alias").writeIndex());
         assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
             equalTo(after.metaData().index("test")));
@@ -259,7 +262,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
             .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build();
 
         ClusterState after = service.applyAliasActions(before, Arrays.asList(
-            new AliasAction.Add("test", "alias", null, null, null, null)));
+            new AliasAction.Add("test", "alias", null, null, null, null, null)));
         assertNull(after.metaData().index("test").getAliases().get("alias").writeIndex());
         assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
             equalTo(after.metaData().index("test2")));
@@ -267,7 +270,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         assertAliasesVersionUnchanged("test2", before, after);
 
         Exception exception = expectThrows(IllegalStateException.class, () -> service.applyAliasActions(before, Arrays.asList(
-            new AliasAction.Add("test", "alias", null, null, null, true))));
+            new AliasAction.Add("test", "alias", null, null, null, true, null))));
         assertThat(exception.getMessage(), startsWith("alias [alias] has more than one write index ["));
     }
 
@@ -282,8 +285,8 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
 
         Boolean unsetValue = randomBoolean() ? null : false;
         List<AliasAction> swapActions = Arrays.asList(
-            new AliasAction.Add("test", "alias", null, null, null, unsetValue),
-            new AliasAction.Add("test2", "alias", null, null, null, true)
+            new AliasAction.Add("test", "alias", null, null, null, unsetValue, null),
+            new AliasAction.Add("test2", "alias", null, null, null, true, null)
         );
         Collections.shuffle(swapActions, random());
         ClusterState after = service.applyAliasActions(before, swapActions);
@@ -310,7 +313,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         assertNull(((AliasOrIndex.Alias) before.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex());
 
         ClusterState after = service.applyAliasActions(before, Arrays.asList(
-            new AliasAction.Add("test3", "alias", null, null, null, true)));
+            new AliasAction.Add("test3", "alias", null, null, null, true, null)));
         assertTrue(after.metaData().index("test3").getAliases().get("alias").writeIndex());
         assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
             equalTo(after.metaData().index("test3")));
@@ -349,12 +352,108 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
             .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build();
 
         Exception exception = expectThrows(IllegalStateException.class, () -> service.applyAliasActions(before, Arrays.asList(
-            new AliasAction.Add("test", "alias", null, null, null, true),
-            new AliasAction.Add("test2", "alias", null, null, null, true)
+            new AliasAction.Add("test", "alias", null, null, null, true, null),
+            new AliasAction.Add("test2", "alias", null, null, null, true, null)
         )));
         assertThat(exception.getMessage(), startsWith("alias [alias] has more than one write index ["));
     }
 
+    public void testHiddenPropertyValidation() {
+        ClusterState originalState = ClusterState.EMPTY_STATE;
+        originalState = createIndex(originalState, "test1");
+        originalState = createIndex(originalState, "test2");
+
+        {
+            // Add a non-hidden alias to one index
+            ClusterState testState = service.applyAliasActions(originalState, Collections.singletonList(
+                new AliasAction.Add("test1", "alias", null, null, null, null, randomFrom(false, null))
+            ));
+
+            // Adding the same alias as hidden to another index should throw
+            Exception ex = expectThrows(IllegalStateException.class, () -> // Add a non-hidden alias to one index
+                service.applyAliasActions(testState, Collections.singletonList(
+                    new AliasAction.Add("test2", "alias", null, null, null, null, true)
+                )));
+            assertThat(ex.getMessage(), containsString("alias [alias] has is_hidden set to true on indices"));
+        }
+
+        {
+            // Add a hidden alias to one index
+            ClusterState testState = service.applyAliasActions(originalState, Collections.singletonList(
+                new AliasAction.Add("test1", "alias", null, null, null, null, true)
+            ));
+
+            // Adding the same alias as non-hidden to another index should throw
+            Exception ex = expectThrows(IllegalStateException.class, () -> // Add a non-hidden alias to one index
+                service.applyAliasActions(testState, Collections.singletonList(
+                    new AliasAction.Add("test2", "alias", null, null, null, null, randomFrom(false, null))
+                )));
+            assertThat(ex.getMessage(), containsString("alias [alias] has is_hidden set to true on indices"));
+        }
+
+        {
+            // Add a non-hidden alias to one index
+            ClusterState testState = service.applyAliasActions(originalState, Collections.singletonList(
+                new AliasAction.Add("test1", "alias", null, null, null, null, randomFrom(false, null))
+            ));
+
+            // Adding the same alias as non-hidden should be OK
+            service.applyAliasActions(testState, Collections.singletonList(
+                    new AliasAction.Add("test2", "alias", null, null, null, null, randomFrom(false, null))
+            ));
+        }
+
+        {
+            // Add a hidden alias to one index
+            ClusterState testState = service.applyAliasActions(originalState, Collections.singletonList(
+                new AliasAction.Add("test1", "alias", null, null, null, null, true)
+            ));
+
+            // Adding the same alias as hidden should be OK
+            service.applyAliasActions(testState, Collections.singletonList(
+                new AliasAction.Add("test2", "alias", null, null, null, null, true)
+            ));
+        }
+    }
+
+    public void testSimultaneousHiddenPropertyValidation() {
+        IndexMetaData.Builder indexMetaData = IndexMetaData.builder("test")
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        IndexMetaData.Builder indexMetaData2 = IndexMetaData.builder("test2")
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        ClusterState before = ClusterState.builder(ClusterName.DEFAULT)
+            .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build();
+
+        {
+            // These should all be fine
+            applyHiddenAliasMix(before, null, null);
+            applyHiddenAliasMix(before, false, false);
+            applyHiddenAliasMix(before, false, null);
+            applyHiddenAliasMix(before, null, false);
+
+            applyHiddenAliasMix(before, true, true);
+        }
+
+        {
+            Exception exception = expectThrows(IllegalStateException.class,
+                () -> applyHiddenAliasMix(before, true, randomFrom(false, null)));
+            assertThat(exception.getMessage(), startsWith("alias [alias] has is_hidden set to true on indices ["));
+        }
+
+        {
+            Exception exception = expectThrows(IllegalStateException.class,
+                () -> applyHiddenAliasMix(before, randomFrom(false, null), true));
+            assertThat(exception.getMessage(), startsWith("alias [alias] has is_hidden set to true on indices ["));
+        }
+    }
+
+    private ClusterState applyHiddenAliasMix(ClusterState before, Boolean isHidden1, Boolean isHidden2) {
+        return service.applyAliasActions(before, Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, null, isHidden1),
+            new AliasAction.Add("test2", "alias", null, null, null, null, isHidden2)
+        ));
+    }
+
     private ClusterState createIndex(ClusterState state, String index) {
         IndexMetaData indexMetaData = IndexMetaData.builder(index)
                 .settings(Settings.builder().put("index.version.created", VersionUtils.randomVersion(random())))

+ 33 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java

@@ -226,6 +226,39 @@ public class MetaDataTests extends ESTestCase {
         assertThat(exception.getMessage(), startsWith("alias [" + alias + "] has more than one write index ["));
     }
 
+    public void testValidateHiddenAliasConsistency() {
+        String alias = randomAlphaOfLength(5);
+        String indexA = randomAlphaOfLength(6);
+        String indexB = randomAlphaOfLength(7);
+
+        {
+            Exception ex = expectThrows(IllegalStateException.class,
+                () -> buildMetadataWithHiddenIndexMix(alias, indexA, true, indexB, randomFrom(false, null)).build());
+            assertThat(ex.getMessage(), containsString("has is_hidden set to true on indices"));
+        }
+
+        {
+            Exception ex = expectThrows(IllegalStateException.class,
+                () -> buildMetadataWithHiddenIndexMix(alias, indexA, randomFrom(false, null), indexB, true).build());
+            assertThat(ex.getMessage(), containsString("has is_hidden set to true on indices"));
+        }
+    }
+
+    private MetaData.Builder buildMetadataWithHiddenIndexMix(String aliasName, String indexAName, Boolean indexAHidden,
+                                                             String indexBName, Boolean indexBHidden) {
+        IndexMetaData.Builder indexAMeta = IndexMetaData.builder(indexAName)
+            .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .putAlias(AliasMetaData.builder(aliasName).isHidden(indexAHidden).build());
+        IndexMetaData.Builder indexBMeta = IndexMetaData.builder(indexBName)
+            .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .putAlias(AliasMetaData.builder(aliasName).isHidden(indexBHidden).build());
+        return MetaData.builder().put(indexAMeta).put(indexBMeta);
+    }
+
     public void testResolveIndexRouting() {
         IndexMetaData.Builder builder = IndexMetaData.builder("index")
                 .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT))

+ 3 - 0
test/framework/src/main/java/org/elasticsearch/index/alias/RandomAliasActionsGenerator.java

@@ -85,6 +85,9 @@ public final class RandomAliasActionsGenerator {
             if (randomBoolean()) {
                 action.writeIndex(randomBoolean());
             }
+            if (randomBoolean()) {
+                action.isHidden(randomBoolean());
+            }
         }
         return action;
     }

+ 8 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java

@@ -417,15 +417,21 @@ class IndicesAndAliasesResolver {
     private static boolean isIndexVisible(String expression, String index, IndicesOptions indicesOptions, MetaData metaData,
                                           boolean dateMathExpression) {
         AliasOrIndex aliasOrIndex = metaData.getAliasAndIndexLookup().get(index);
+        final boolean isHidden = aliasOrIndex.isHidden();
         if (aliasOrIndex.isAlias()) {
             //it's an alias, ignore expandWildcardsOpen and expandWildcardsClosed.
             //complicated to support those options with aliases pointing to multiple indices...
             //TODO investigate supporting expandWildcards option for aliases too, like es core does.
-            return indicesOptions.ignoreAliases() == false;
+            if (indicesOptions.ignoreAliases()) {
+                return false;
+            } else if (isHidden == false || indicesOptions.expandWildcardsHidden() || isVisibleDueToImplicitHidden(expression, index)) {
+                return true;
+            } else {
+                return false;
+            }
         }
         assert aliasOrIndex.getIndices().size() == 1 : "concrete index must point to a single index";
         IndexMetaData indexMetaData = aliasOrIndex.getIndices().get(0);
-        final boolean isHidden = IndexMetaData.INDEX_HIDDEN_SETTING.get(indexMetaData.getSettings());
         if (isHidden && indicesOptions.expandWildcardsHidden() == false && isVisibleDueToImplicitHidden(expression, index) == false) {
             return false;
         }

+ 64 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java

@@ -146,6 +146,15 @@ public class IndicesAndAliasesResolverTests extends ESTestCase {
                     .settings(Settings.builder().put(settings).put("index.hidden", true).build()))
                 .put(indexBuilder("hidden-closed").state(State.CLOSE)
                     .settings(Settings.builder().put(settings).put("index.hidden", true).build()))
+                .put(indexBuilder("hidden-w-aliases").settings(Settings.builder().put(settings).put("index.hidden", true).build())
+                    .putAlias(AliasMetaData.builder("alias-hidden").isHidden(true).build())
+                    .putAlias(AliasMetaData.builder(".alias-hidden").isHidden(true).build())
+                    .putAlias(AliasMetaData.builder("alias-visible-mixed").isHidden(false).build()))
+                .put(indexBuilder("hidden-w-visible-alias").settings(Settings.builder().put(settings).put("index.hidden", true).build())
+                    .putAlias(AliasMetaData.builder("alias-visible").build()))
+                .put(indexBuilder("visible-w-aliases").settings(Settings.builder().put(settings).build())
+                    .putAlias(AliasMetaData.builder("alias-visible").build())
+                    .putAlias(AliasMetaData.builder("alias-visible-mixed").isHidden(false).build()))
                 .put(indexBuilder(securityIndexName).settings(settings)).build();
 
         if (withAlias) {
@@ -169,6 +178,13 @@ public class IndicesAndAliasesResolverTests extends ESTestCase {
         roleMap.put("alias_read_write", new RoleDescriptor("alias_read_write", null,
             new IndicesPrivileges[] { IndicesPrivileges.builder().indices("barbaz", "foofoobar").privileges("read", "write").build() },
             null));
+        roleMap.put("hidden_alias_test", new RoleDescriptor("hidden_alias_test", null,
+            new IndicesPrivileges[] {
+                IndicesPrivileges.builder()
+                    .indices("alias-visible", "alias-visible-mixed", "alias-hidden", ".alias-hidden", "hidden-open")
+                    .privileges("all")
+                    .build()
+            }, null));
         roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR);
         final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
         doAnswer((i) -> {
@@ -1388,7 +1404,6 @@ public class IndicesAndAliasesResolverTests extends ESTestCase {
         // open + hidden
         searchRequest = new SearchRequest();
         searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, true));
-        authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME);
         resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases(searchRequest, metaData, authorizedIndices);
         assertThat(resolvedIndices.getLocal(),
             containsInAnyOrder("bar", "foofoobar", "foobarfoo", "foofoo", "hidden-open", ".hidden-open"));
@@ -1427,6 +1442,54 @@ public class IndicesAndAliasesResolverTests extends ESTestCase {
         assertThat(resolvedIndices.getRemote(), emptyIterable());
     }
 
+    public void testHiddenAliasesResolution() {
+        final User user = new User("hidden-alias-tester", "hidden_alias_test");
+        final List<String> authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME);
+
+        // Visible only
+        SearchRequest searchRequest = new SearchRequest();
+        searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, false));
+        ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases(searchRequest, metaData, authorizedIndices);
+        assertThat(resolvedIndices.getLocal(), containsInAnyOrder("alias-visible", "alias-visible-mixed"));
+        assertThat(resolvedIndices.getRemote(), emptyIterable());
+
+        // Include hidden explicitly
+        searchRequest = new SearchRequest();
+        searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, true));
+        resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases(searchRequest, metaData, authorizedIndices);
+        assertThat(resolvedIndices.getLocal(),
+            containsInAnyOrder("alias-visible", "alias-visible-mixed", "alias-hidden", ".alias-hidden", "hidden-open"));
+        assertThat(resolvedIndices.getRemote(), emptyIterable());
+
+        // Include hidden with a wildcard
+        searchRequest = new SearchRequest("alias-h*");
+        searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, true));
+        resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases(searchRequest, metaData, authorizedIndices);
+        assertThat(resolvedIndices.getLocal(), containsInAnyOrder("alias-hidden"));
+        assertThat(resolvedIndices.getRemote(), emptyIterable());
+
+        // Dot prefix, implicitly including hidden
+        searchRequest = new SearchRequest(".a*");
+        searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, false));
+        resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases(searchRequest, metaData, authorizedIndices);
+        assertThat(resolvedIndices.getLocal(), containsInAnyOrder(".alias-hidden"));
+        assertThat(resolvedIndices.getRemote(), emptyIterable());
+
+        // Make sure ignoring aliases works (visible only)
+        searchRequest = new SearchRequest();
+        searchRequest.indicesOptions(IndicesOptions.fromOptions(false, true, true, false, false, true, false, true, false));
+        resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases(searchRequest, metaData, authorizedIndices);
+        assertThat(resolvedIndices.getLocal(), contains("-*"));
+        assertThat(resolvedIndices.getRemote(), emptyIterable());
+
+        // Make sure ignoring aliases works (including hidden)
+        searchRequest = new SearchRequest();
+        searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, true, true, false, true, false));
+        resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases(searchRequest, metaData, authorizedIndices);
+        assertThat(resolvedIndices.getLocal(), containsInAnyOrder("hidden-open"));
+        assertThat(resolvedIndices.getRemote(), emptyIterable());
+    }
+
     private List<String> buildAuthorizedIndices(User user, String action) {
         PlainActionFuture<Role> rolesListener = new PlainActionFuture<>();
         final Authentication authentication =