diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContext.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContext.scala index c921286ea0..c2cd052696 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContext.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContext.scala @@ -16,9 +16,10 @@ */ package tech.beshu.ror.accesscontrol.blocks +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext.Indices import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext.TemplatesTransformation -import tech.beshu.ror.accesscontrol.blocks.BlockContextUpdater.{AliasRequestBlockContextUpdater, RepositoryRequestBlockContextUpdater, SnapshotRequestBlockContextUpdater, TemplateRequestBlockContextUpdater} +import tech.beshu.ror.accesscontrol.blocks.BlockContextUpdater._ import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage import tech.beshu.ror.accesscontrol.domain._ @@ -73,6 +74,22 @@ object BlockContext { allAllowedIndices: Set[ClusterIndexName]) extends BlockContext + final case class DataStreamRequestBlockContext(override val requestContext: RequestContext, + override val userMetadata: UserMetadata, + override val responseHeaders: Set[Header], + override val responseTransformations: List[ResponseTransformation], + dataStreams: Set[DataStreamName], + backingIndices: DataStreamRequestBlockContext.BackingIndices) + extends BlockContext + object DataStreamRequestBlockContext { + sealed trait BackingIndices + object BackingIndices { + final case class IndicesInvolved(filteredIndices: Set[ClusterIndexName], + allAllowedIndices: Set[ClusterIndexName]) extends BackingIndices + case object IndicesNotInvolved extends BackingIndices + } + } + final case class AliasRequestBlockContext(override val requestContext: RequestContext, override val userMetadata: UserMetadata, override val responseHeaders: Set[Header], @@ -166,6 +183,13 @@ object BlockContext { override def indices(blockContext: SnapshotRequestBlockContext): Set[ClusterIndexName] = blockContext.filteredIndices } + implicit val indicesFromDataStreamRequestBlockContext = new HasIndices[DataStreamRequestBlockContext] { + override def indices(blockContext: DataStreamRequestBlockContext): Set[ClusterIndexName] = blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => filteredIndices + case BackingIndices.IndicesNotInvolved => Set.empty + } + } + implicit class Ops[B <: BlockContext : HasIndices](blockContext: B) { def indices: Set[ClusterIndexName] = HasIndices[B].indices(blockContext) } @@ -278,6 +302,12 @@ object BlockContext { } } + implicit class DataStreamOperationBlockContextUpdateOps(val blockContext: DataStreamRequestBlockContext) extends AnyVal { + def withDataStreams(dataStreams: Set[DataStreamName]): DataStreamRequestBlockContext = { + DataStreamRequestBlockContextUpdater.withDataStreams(blockContext, dataStreams) + } + } + implicit class BlockContextWithIndicesUpdaterOps[B <: BlockContext : BlockContextWithIndicesUpdater](blockContext: B) { def withIndices(filteredIndices: Set[ClusterIndexName], allAllowedIndices: Set[ClusterIndexName]): B = { BlockContextWithIndicesUpdater[B].withIndices(blockContext, filteredIndices, allAllowedIndices) @@ -331,6 +361,10 @@ object BlockContext { case _: GeneralNonIndexRequestBlockContext => Set.empty case _: RepositoryRequestBlockContext => Set.empty case bc: SnapshotRequestBlockContext => bc.filteredIndices + case bc: DataStreamRequestBlockContext => bc.backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => filteredIndices + case BackingIndices.IndicesNotInvolved => Set.empty + } case bc: TemplateRequestBlockContext => bc.templateOperation match { case TemplateOperation.GettingLegacyAndIndexTemplates(_, _) => Set.empty @@ -371,6 +405,7 @@ object BlockContext { case _: GeneralNonIndexRequestBlockContext => Set.empty case bc: RepositoryRequestBlockContext => bc.repositories case bc: SnapshotRequestBlockContext => bc.repositories + case _: DataStreamRequestBlockContext => Set.empty case _: TemplateRequestBlockContext => Set.empty case _: AliasRequestBlockContext => Set.empty case _: GeneralIndexRequestBlockContext => Set.empty @@ -389,6 +424,26 @@ object BlockContext { case _: GeneralNonIndexRequestBlockContext => Set.empty case _: RepositoryRequestBlockContext => Set.empty case bc: SnapshotRequestBlockContext => bc.snapshots + case _: DataStreamRequestBlockContext => Set.empty + case _: TemplateRequestBlockContext => Set.empty + case _: AliasRequestBlockContext => Set.empty + case _: GeneralIndexRequestBlockContext => Set.empty + case _: MultiIndexRequestBlockContext => Set.empty + case _: FilterableRequestBlockContext => Set.empty + case _: FilterableMultiRequestBlockContext => Set.empty + case _: RorApiRequestBlockContext => Set.empty + } + } + } + + implicit class DataStreamsFromBlockContext(val blockContext: BlockContext) extends AnyVal { + def dataStreams: Set[DataStreamName] = { + blockContext match { + case _: CurrentUserMetadataRequestBlockContext => Set.empty + case _: GeneralNonIndexRequestBlockContext => Set.empty + case _: RepositoryRequestBlockContext => Set.empty + case _: SnapshotRequestBlockContext => Set.empty + case bc: DataStreamRequestBlockContext => bc.dataStreams case _: TemplateRequestBlockContext => Set.empty case _: AliasRequestBlockContext => Set.empty case _: GeneralIndexRequestBlockContext => Set.empty @@ -407,6 +462,7 @@ object BlockContext { case _: GeneralNonIndexRequestBlockContext => None case _: RepositoryRequestBlockContext => None case _: SnapshotRequestBlockContext => None + case _: DataStreamRequestBlockContext => None case bc: TemplateRequestBlockContext => Some(bc.templateOperation) case _: AliasRequestBlockContext => None case _: GeneralIndexRequestBlockContext => None @@ -425,6 +481,7 @@ object BlockContext { case _: GeneralNonIndexRequestBlockContext => None case _: RepositoryRequestBlockContext => None case _: SnapshotRequestBlockContext => None + case _: DataStreamRequestBlockContext => None case _: TemplateRequestBlockContext => None case _: AliasRequestBlockContext => None case _: GeneralIndexRequestBlockContext => None @@ -454,6 +511,7 @@ object BlockContext { case _: GeneralNonIndexRequestBlockContext => hasIndices[GeneralNonIndexRequestBlockContext] case _: RepositoryRequestBlockContext => hasIndices[RepositoryRequestBlockContext] case _: SnapshotRequestBlockContext => hasIndices[SnapshotRequestBlockContext] + case _: DataStreamRequestBlockContext => hasIndices[DataStreamRequestBlockContext] case _: AliasRequestBlockContext => hasIndices[AliasRequestBlockContext] case _: TemplateRequestBlockContext => hasIndices[TemplateRequestBlockContext] case _: GeneralIndexRequestBlockContext => hasIndices[GeneralIndexRequestBlockContext] diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContextUpdater.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContextUpdater.scala index 3d9d16a1fd..50b90bfd00 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContextUpdater.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/BlockContextUpdater.scala @@ -127,6 +127,30 @@ object BlockContextUpdater { blockContext.copy(filteredIndices = indices) } + implicit object DataStreamRequestBlockContextUpdater + extends BlockContextUpdater[DataStreamRequestBlockContext] { + + override def emptyBlockContext(blockContext: DataStreamRequestBlockContext): DataStreamRequestBlockContext = + DataStreamRequestBlockContext(blockContext.requestContext, UserMetadata.empty, Set.empty, List.empty, Set.empty, DataStreamRequestBlockContext.BackingIndices.IndicesNotInvolved) + + override def withUserMetadata(blockContext: DataStreamRequestBlockContext, + userMetadata: UserMetadata): DataStreamRequestBlockContext = + blockContext.copy(userMetadata = userMetadata) + + override def withAddedResponseHeader(blockContext: DataStreamRequestBlockContext, + header: Header): DataStreamRequestBlockContext = + blockContext.copy(responseHeaders = blockContext.responseHeaders + header) + + override def withAddedResponseTransformation(blockContext: DataStreamRequestBlockContext, + responseTransformation: ResponseTransformation): DataStreamRequestBlockContext = + blockContext.copy(responseTransformations = responseTransformation :: blockContext.responseTransformations) + + def withDataStreams(blockContext: DataStreamRequestBlockContext, + dataStreams: Set[DataStreamName]): DataStreamRequestBlockContext = { + blockContext.copy(dataStreams = dataStreams) + } + } + implicit object TemplateRequestBlockContextUpdater extends BlockContextUpdater[TemplateRequestBlockContext] { @@ -285,7 +309,7 @@ object BlockContextUpdater { } } -abstract class BlockContextWithIndicesUpdater[B <: BlockContext: HasIndices] { +abstract class BlockContextWithIndicesUpdater[B <: BlockContext : HasIndices] { def withIndices(blockContext: B, filteredIndices: Set[ClusterIndexName], allAllowedIndices: Set[ClusterIndexName]): B } @@ -319,6 +343,18 @@ object BlockContextWithIndicesUpdater { allAllowedIndices: Set[ClusterIndexName]): SnapshotRequestBlockContext = blockContext.copy(filteredIndices = filteredIndices, allAllowedIndices = allAllowedIndices) } + + implicit object DataStreamRequestBlocContextWithIndicesUpdater + extends BlockContextWithIndicesUpdater[DataStreamRequestBlockContext] { + + def withIndices(blockContext: DataStreamRequestBlockContext, + filteredIndices: Set[ClusterIndexName], + allAllowedIndices: Set[ClusterIndexName]): DataStreamRequestBlockContext = + blockContext.copy(backingIndices = DataStreamRequestBlockContext.BackingIndices.IndicesInvolved( + filteredIndices = filteredIndices, + allAllowedIndices = allAllowedIndices + )) + } } abstract class BlockContextWithIndexPacksUpdater[B <: BlockContext : HasIndexPacks] { diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala index 9ac4f7e5d7..b4dd93cee3 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala @@ -61,6 +61,7 @@ object ImpersonationWarning { implicit val actionsRule: ImpersonationWarningSupport[ActionsRule] = NotSupported implicit val apiKeyRule: ImpersonationWarningSupport[ApiKeysRule] = NotSupported + implicit val dataStreamsRule: ImpersonationWarningSupport[DataStreamsRule] = NotSupported implicit val fieldsRule: ImpersonationWarningSupport[FieldsRule] = NotSupported implicit val filterRule: ImpersonationWarningSupport[FilterRule] = NotSupported implicit val headersAndRule: ImpersonationWarningSupport[HeadersAndRule] = NotSupported diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/DataStreamsRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/DataStreamsRule.scala new file mode 100644 index 0000000000..e970e290a6 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/DataStreamsRule.scala @@ -0,0 +1,102 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.blocks.rules + +import cats.data.NonEmptySet +import cats.implicits._ +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.rules.DataStreamsRule.Settings +import tech.beshu.ror.accesscontrol.blocks.rules.base.Rule +import tech.beshu.ror.accesscontrol.blocks.rules.base.Rule.RuleResult.{Fulfilled, Rejected} +import tech.beshu.ror.accesscontrol.blocks.rules.base.Rule.{RegularRule, RuleName, RuleResult} +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable +import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.accesscontrol.matchers.ZeroKnowledgeDataStreamsFilterScalaAdapter.CheckResult +import tech.beshu.ror.accesscontrol.matchers.{MatcherWithWildcardsScalaAdapter, ZeroKnowledgeDataStreamsFilterScalaAdapter} +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.accesscontrol.utils.RuntimeMultiResolvableVariableOps.resolveAll +import tech.beshu.ror.utils.ZeroKnowledgeIndexFilter + +class DataStreamsRule(val settings: Settings) + extends RegularRule + with Logging { + + override val name: Rule.Name = DataStreamsRule.Name.name + + private val zeroKnowledgeMatchFilter = new ZeroKnowledgeDataStreamsFilterScalaAdapter( + new ZeroKnowledgeIndexFilter(true) + ) + + override def regularCheck[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[Rule.RuleResult[B]] = Task { + BlockContextUpdater[B] match { + case BlockContextUpdater.DataStreamRequestBlockContextUpdater => + checkDataStreams(blockContext) + case _ => + Fulfilled(blockContext) + } + } + + private def checkDataStreams(blockContext: DataStreamRequestBlockContext): RuleResult[DataStreamRequestBlockContext] = { + checkAllowedDataStreams( + resolveAll(settings.allowedDataStreams.toNonEmptyList, blockContext).toSet, + blockContext.dataStreams, + blockContext.requestContext + ) match { + case Right(filteredDataStreams) => Fulfilled(blockContext.withDataStreams(filteredDataStreams)) + case Left(()) => Rejected() + } + } + + private def checkAllowedDataStreams(allowedDataStreams: Set[DataStreamName], + dataStreamsToCheck: Set[DataStreamName], + requestContext: RequestContext) = { + if (allowedDataStreams.contains(DataStreamName.All) || allowedDataStreams.contains(DataStreamName.Wildcard)) { + Right(dataStreamsToCheck) + } else { + zeroKnowledgeMatchFilter.check( + dataStreamsToCheck, + MatcherWithWildcardsScalaAdapter.create(allowedDataStreams) + ) match { + case CheckResult.Ok(processedDataStreams) if requestContext.isReadOnlyRequest => + Right(processedDataStreams) + case CheckResult.Ok(processedDataStreams) if processedDataStreams.size === dataStreamsToCheck.size => + Right(processedDataStreams) + case CheckResult.Ok(processedDataStreams) => + val filteredOutDataStreams = dataStreamsToCheck.diff(processedDataStreams).map(_.show) + logger.debug( + s"[${requestContext.id.show}] Write request with data streams cannot proceed because some of the data streams " + + s"[${filteredOutDataStreams.toList.mkString_(",")}] were filtered out by ACL. The request will be rejected.." + ) + Left(()) + case CheckResult.Failed => + logger.debug(s"[${requestContext.id.show}] The processed data streams do not match the allowed data streams. The request will be rejected..") + Left(()) + } + } + } +} + +object DataStreamsRule { + implicit case object Name extends RuleName[DataStreamsRule] { + override val name: Rule.Name = Rule.Name("data_streams") + } + + final case class Settings(allowedDataStreams: NonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]]) +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FieldsRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FieldsRule.scala index cbc693b5da..afae2f9bc2 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FieldsRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FieldsRule.scala @@ -58,6 +58,7 @@ class FieldsRule(val settings: Settings) case GeneralNonIndexRequestBlockContextUpdater => RuleResult.Fulfilled(blockContext) case RepositoryRequestBlockContextUpdater => RuleResult.Fulfilled(blockContext) case SnapshotRequestBlockContextUpdater => RuleResult.Fulfilled(blockContext) + case DataStreamRequestBlockContextUpdater => RuleResult.Fulfilled(blockContext) case TemplateRequestBlockContextUpdater => RuleResult.Fulfilled(blockContext) case GeneralIndexRequestBlockContextUpdater => RuleResult.Fulfilled(blockContext) case MultiIndexRequestBlockContextUpdater => RuleResult.Fulfilled(blockContext) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FilterRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FilterRule.scala index f19e46057d..2c85454664 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FilterRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/FilterRule.scala @@ -50,6 +50,7 @@ class FilterRule(val settings: Settings) case GeneralNonIndexRequestBlockContextUpdater => Fulfilled(blockContext) case RepositoryRequestBlockContextUpdater => Fulfilled(blockContext) case SnapshotRequestBlockContextUpdater => Fulfilled(blockContext) + case DataStreamRequestBlockContextUpdater => Fulfilled(blockContext) case TemplateRequestBlockContextUpdater => Fulfilled(blockContext) case GeneralIndexRequestBlockContextUpdater => Fulfilled(blockContext) case AliasRequestBlockContextUpdater => Fulfilled(blockContext) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/RepositoriesRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/RepositoriesRule.scala index b199c87338..c2dcf1d555 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/RepositoriesRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/RepositoriesRule.scala @@ -19,6 +19,7 @@ package tech.beshu.ror.accesscontrol.blocks.rules import cats.data.NonEmptySet import cats.implicits._ import monix.eval.Task +import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.blocks.BlockContext.{RepositoryRequestBlockContext, _} import tech.beshu.ror.accesscontrol.blocks.rules.RepositoriesRule.Settings import tech.beshu.ror.accesscontrol.blocks.rules.base.Rule @@ -34,7 +35,8 @@ import tech.beshu.ror.accesscontrol.utils.RuntimeMultiResolvableVariableOps.reso import tech.beshu.ror.utils.ZeroKnowledgeIndexFilter class RepositoriesRule(val settings: Settings) - extends RegularRule { + extends RegularRule + with Logging { override val name: Rule.Name = RepositoriesRule.Name.name @@ -87,7 +89,15 @@ class RepositoriesRule(val settings: Settings) Right(processedRepositories) case CheckResult.Ok(processedRepositories) if processedRepositories.size === repositoriesToCheck.size => Right(processedRepositories) - case CheckResult.Ok(_) | CheckResult.Failed => + case CheckResult.Ok(processedRepositories) => + val filteredOutRepositories = repositoriesToCheck.diff(processedRepositories).map(_.show) + logger.debug( + s"[${requestContext.id.show}] Write request with repositories cannot proceed because some of the repositories " + + s"[${filteredOutRepositories.toList.mkString_(",")}] were filtered out by ACL. The request will be rejected.." + ) + Left(()) + case CheckResult.Failed => + logger.debug(s"[${requestContext.id.show}] The processed repositories do not match the allowed repositories. The request will be rejected..") Left(()) } } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/indicesrule/IndicesRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/indicesrule/IndicesRule.scala index 3549ec3ca3..24d944f8de 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/indicesrule/IndicesRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/indicesrule/IndicesRule.scala @@ -20,8 +20,9 @@ import cats.data.NonEmptySet import cats.implicits._ import monix.eval.Task import org.apache.logging.log4j.scala.Logging +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext.Indices -import tech.beshu.ror.accesscontrol.blocks.BlockContext.{AliasRequestBlockContext, HasIndexPacks, SnapshotRequestBlockContext} +import tech.beshu.ror.accesscontrol.blocks.BlockContext.{AliasRequestBlockContext, DataStreamRequestBlockContext, HasIndexPacks, SnapshotRequestBlockContext} import tech.beshu.ror.accesscontrol.blocks.BlockContextUpdater._ import tech.beshu.ror.accesscontrol.blocks.rules.base.Rule.RuleResult.Rejected.Cause import tech.beshu.ror.accesscontrol.blocks.rules.base.Rule.RuleResult.{Fulfilled, Rejected} @@ -54,6 +55,7 @@ class IndicesRule(override val settings: Settings, case GeneralNonIndexRequestBlockContextUpdater => processRequestWithoutIndices(blockContext) case RepositoryRequestBlockContextUpdater => processRequestWithoutIndices(blockContext) case SnapshotRequestBlockContextUpdater => processSnapshotRequest(blockContext) + case DataStreamRequestBlockContextUpdater => processDataStreamRequest(blockContext) case GeneralIndexRequestBlockContextUpdater => processIndicesRequest(blockContext) case FilterableRequestBlockContextUpdater => processIndicesRequest(blockContext) case MultiIndexRequestBlockContextUpdater => processIndicesPacks(blockContext) @@ -76,7 +78,8 @@ class IndicesRule(override val settings: Settings, val allAllowedIndices = resolveAll(settings.allowedIndices.toNonEmptyList, blockContext).toSet processIndices(blockContext.requestContext, allAllowedIndices, blockContext.indices) .map { - case ProcessResult.Ok(filteredIndices) => Fulfilled(blockContext.withIndices(filteredIndices, allAllowedIndices)) + case ProcessResult.Ok(filteredIndices) => + Fulfilled(blockContext.withIndices(filteredIndices, allAllowedIndices)) case ProcessResult.Failed(cause) => Rejected(cause) } } @@ -146,6 +149,12 @@ class IndicesRule(override val settings: Settings, else processIndicesRequest(blockContext) } + private def processDataStreamRequest(blockContext: DataStreamRequestBlockContext): Task[RuleResult[DataStreamRequestBlockContext]] = { + if (matchAll) Task.now(Fulfilled(blockContext)) + else if (blockContext.backingIndices == BackingIndices.IndicesNotInvolved) processRequestWithoutIndices(blockContext) + else processIndicesRequest(blockContext) + } + private val matchAll = settings.allowedIndices.exists { case AlreadyResolved(indices) if indices.exists(_.allIndicesRequested) => true case _ => false diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala index 025529d5b6..d103dd8c11 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala @@ -43,6 +43,7 @@ object LocalUsersContext { implicit val actionsRule: LocalUsersSupport[ActionsRule] = NotAvailableLocalUsers implicit val apiKeyRule: LocalUsersSupport[ApiKeysRule] = NotAvailableLocalUsers + implicit val dataStreamsRule: LocalUsersSupport[DataStreamsRule] = NotAvailableLocalUsers implicit val externalAuthenticationRule: LocalUsersSupport[ExternalAuthenticationRule] = NotAvailableLocalUsers implicit val externalAuthorizationRule: LocalUsersSupport[ExternalAuthorizationRule] = NotAvailableLocalUsers implicit val fieldsRule: LocalUsersSupport[FieldsRule] = NotAvailableLocalUsers diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala index 2b9f82af9c..e76aadfc84 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala @@ -47,6 +47,7 @@ object VariableContext { case object NotUsingVariable extends VariableUsage[Nothing] + implicit val dataStreamsRule: VariableUsage[DataStreamsRule] = UsingVariable[DataStreamsRule](rule => rule.settings.allowedDataStreams.toNonEmptyList) implicit val filterRule: VariableUsage[FilterRule] = UsingVariable[FilterRule](rule => NonEmptyList.one(rule.settings.filter)) implicit val groupsOrRule: VariableUsage[GroupsOrRule] = UsingVariable[GroupsOrRule](rule => rule.settings.permittedGroups.permittedGroups.toNonEmptyList) implicit val groupsAndRule: VariableUsage[GroupsAndRule] = UsingVariable[GroupsAndRule](rule => rule.settings.permittedGroups.permittedGroups.toNonEmptyList) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/domain.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/domain.scala index c4feff61de..de466bdafb 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/domain.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/domain.scala @@ -31,7 +31,6 @@ import tech.beshu.ror.Constants import tech.beshu.ror.accesscontrol.blocks.BlockContext import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable import tech.beshu.ror.accesscontrol.domain.Action._ -import tech.beshu.ror.accesscontrol.show.logs._ import tech.beshu.ror.accesscontrol.domain.ClusterIndexName.Remote.ClusterName import tech.beshu.ror.accesscontrol.domain.ClusterIndexName.{Local, Remote} import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions.{AccessMode, DocumentField} @@ -40,6 +39,7 @@ import tech.beshu.ror.accesscontrol.domain.GroupLike.GroupName import tech.beshu.ror.accesscontrol.domain.Header.AuthorizationValueError.{EmptyAuthorizationValue, InvalidHeaderFormat, RorMetadataInvalidFormat} import tech.beshu.ror.accesscontrol.header.ToHeaderValue import tech.beshu.ror.accesscontrol.matchers.{IndicesNamesMatcher, MatcherWithWildcardsScalaAdapter, TemplateNamePatternMatcher, UniqueIdentifierGenerator} +import tech.beshu.ror.accesscontrol.show.logs._ import tech.beshu.ror.accesscontrol.utils.RuntimeMultiResolvableVariableOps.resolveAll import tech.beshu.ror.com.jayway.jsonpath.JsonPath import tech.beshu.ror.utils.CaseMappingEquality @@ -902,7 +902,7 @@ object domain { case Wildcard => "*" } - implicit val eqRepository: Eq[SnapshotName] = Eq.fromUniversalEquals + implicit val eqSnapshotName: Eq[SnapshotName] = Eq.fromUniversalEquals implicit val caseMappingEqualitySnapshotName: CaseMappingEquality[SnapshotName] = CaseMappingEquality.instance( { case Full(value) => value.value @@ -914,6 +914,45 @@ object domain { ) } + sealed trait DataStreamName + object DataStreamName { + final case class Full private(value: NonEmptyString) extends DataStreamName + final case class Pattern private(value: NonEmptyString) extends DataStreamName + case object All extends DataStreamName + case object Wildcard extends DataStreamName + + def all: DataStreamName = DataStreamName.All + + def wildcard: DataStreamName = DataStreamName.Wildcard + + def fromString(value: String): Option[DataStreamName] = { + NonEmptyString.unapply(value).map { + case Refined("_all") => All + case Refined("*") => Wildcard + case v if v.contains("*") => Pattern(NonEmptyString.unsafeFrom(v)) + case v => Full(NonEmptyString.unsafeFrom(v)) + } + } + + def toString(dataStreamName: DataStreamName): String = dataStreamName match { + case Full(value) => value.value + case Pattern(value) => value.value + case All => "_all" + case Wildcard => "*" + } + + implicit val eqDataStreamName: Eq[DataStreamName] = Eq.fromUniversalEquals + implicit val caseMappingEqualityDataStreamName: CaseMappingEquality[DataStreamName] = CaseMappingEquality.instance( + { + case Full(value) => value.value + case Pattern(value) => value.value + case All => "*" + case Wildcard => "*" + }, + identity + ) + } + sealed trait Template { def name: TemplateName } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala index 8afa0fadee..3a74dd3b11 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala @@ -73,6 +73,7 @@ object ruleDecoders { case RepositoriesRule.Name.name => Some(RepositoriesRuleDecoder) case SessionMaxIdleRule.Name.name => Some(new SessionMaxIdleRuleDecoder()) case SnapshotsRule.Name.name => Some(SnapshotsRuleDecoder) + case DataStreamsRule.Name.name => Some(DataStreamsRuleDecoder) case UriRegexRule.Name.name => Some(UriRegexRuleDecoder) case UsersRule.Name.name => Some(new UsersRuleDecoder()(caseMappingEquality)) case XForwardedForRule.Name.name => Some(XForwardedForRuleDecoder) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/DataStreamsRuleDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/DataStreamsRuleDecoder.scala new file mode 100644 index 0000000000..b4d3ce5257 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/DataStreamsRuleDecoder.scala @@ -0,0 +1,98 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.factory.decoders.rules + +import cats.data.NonEmptySet +import cats.implicits._ +import eu.timepit.refined.types.string.NonEmptyString +import io.circe.Decoder +import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition +import tech.beshu.ror.accesscontrol.blocks.rules.DataStreamsRule +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable.{AlreadyResolved, ToBeResolved} +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeResolvableVariable.Convertible +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.{RuntimeMultiResolvableVariable, RuntimeResolvableVariableCreator} +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.accesscontrol.factory.decoders.rules.DataStreamsDecodersHelper._ +import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields +import tech.beshu.ror.accesscontrol.orders._ +import tech.beshu.ror.accesscontrol.show.logs._ +import tech.beshu.ror.accesscontrol.utils.CirceOps._ + +object DataStreamsRuleDecoder + extends RuleBaseDecoderWithoutAssociatedFields[DataStreamsRule] { + + override protected def decoder: Decoder[RuleDefinition[DataStreamsRule]] = + DecoderHelpers + .decodeStringLikeOrNonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]] + .toSyncDecoder + .emapE { dataStreams => + if (checkIfAlreadyResolvedDataStreamVariableContains(dataStreams, DataStreamName.all)) + Left(RulesLevelCreationError(Message(s"Setting up a rule (${DataStreamsRule.Name.show}) that matches all the values is redundant - data stream ${DataStreamName.all.show}"))) + else if (checkIfAlreadyResolvedDataStreamVariableContains(dataStreams, DataStreamName.wildcard)) + Left(RulesLevelCreationError(Message(s"Setting up a rule (${DataStreamsRule.Name.show}) that matches all the values is redundant - data stream ${DataStreamName.wildcard.show}"))) + else + Right(dataStreams) + } + .map(dataStreams => RuleDefinition.create(new DataStreamsRule(DataStreamsRule.Settings(dataStreams)))) + .decoder +} + +private object DataStreamsDecodersHelper { + private implicit val dataStreamNameConvertible: Convertible[DataStreamName] = new Convertible[DataStreamName] { + override def convert: String => Either[Convertible.ConvertError, DataStreamName] = { str => + + for { + dataStreamName <- DataStreamName.fromString(str).toRight(Convertible.ConvertError("Data stream name cannot be empty")) + _ <- validateIsLowerCase(dataStreamName) + } yield dataStreamName + } + + private def validateIsLowerCase(value: DataStreamName) = value match { + case DataStreamName.Full(name) if isLowerCase(name) => Right(()) + case DataStreamName.Pattern(name) if isLowerCase(name) => Right(()) + case DataStreamName.Full(_) | DataStreamName.Pattern(_) => + Left(Convertible.ConvertError("Data stream name cannot contain the upper case characters")) + case DataStreamName.All => Right(()) + case DataStreamName.Wildcard => Right(()) + } + + private def isLowerCase(value: NonEmptyString) = value.value.toLowerCase == value.value + } + + implicit val dataStreamNameValueDecoder: Decoder[RuntimeMultiResolvableVariable[DataStreamName]] = + DecoderHelpers + .decodeStringLikeNonEmpty + .toSyncDecoder + .emapE { str => + RuntimeResolvableVariableCreator + .createMultiResolvableVariableFrom[DataStreamName](str) + .left.map(error => RulesLevelCreationError(Message(error.show))) + } + .decoder + + private[rules] def checkIfAlreadyResolvedDataStreamVariableContains(dataStreamsVars: NonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]], + dataStream: DataStreamName): Boolean = { + dataStreamsVars + .find { + case AlreadyResolved(dataStreams) => dataStreams.contains_(dataStream) + case ToBeResolved(_) => false + } + .isDefined + } +} \ No newline at end of file diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/IndicesRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/IndicesRulesDecoders.scala index fac0ab7dc5..920d31f28a 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/IndicesRulesDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/IndicesRulesDecoders.scala @@ -21,17 +21,14 @@ import cats.implicits._ import io.circe.Decoder import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition import tech.beshu.ror.accesscontrol.blocks.rules.indicesrule.IndicesRule -import tech.beshu.ror.accesscontrol.blocks.rules.{RepositoriesRule, SnapshotsRule} import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable.{AlreadyResolved, ToBeResolved} import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeResolvableVariable.Convertible import tech.beshu.ror.accesscontrol.blocks.variables.runtime.{RuntimeMultiResolvableVariable, RuntimeResolvableVariableCreator} -import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError import tech.beshu.ror.accesscontrol.factory.decoders.rules.IndicesDecodersHelper._ -import tech.beshu.ror.accesscontrol.factory.decoders.rules.RepositoriesDecodersHelper._ import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields -import tech.beshu.ror.accesscontrol.factory.decoders.rules.SnapshotDecodersHelper._ import tech.beshu.ror.accesscontrol.matchers.RandomBasedUniqueIdentifierGenerator import tech.beshu.ror.accesscontrol.orders._ import tech.beshu.ror.accesscontrol.show.logs._ @@ -80,106 +77,6 @@ object IndicesRuleDecoders } } -object SnapshotsRuleDecoder - extends RuleBaseDecoderWithoutAssociatedFields[SnapshotsRule] { - - override protected def decoder: Decoder[RuleDefinition[SnapshotsRule]] = { - DecoderHelpers - .decodeStringLikeOrNonEmptySet[RuntimeMultiResolvableVariable[SnapshotName]] - .toSyncDecoder - .emapE { snapshots => - if (SnapshotDecodersHelper.checkIfAlreadyResolvedSnapshotVariableContains(snapshots, SnapshotName.All)) - Left(RulesLevelCreationError(Message(s"Setting up a rule (${SnapshotsRule.Name.show}) that matches all the values is redundant - snapshot ${SnapshotName.all.show}"))) - else if (SnapshotDecodersHelper.checkIfAlreadyResolvedSnapshotVariableContains(snapshots, SnapshotName.Wildcard)) - Left(RulesLevelCreationError(Message(s"Setting up a rule (${SnapshotsRule.Name.show}) that matches all the values is redundant - snapshot ${SnapshotName.wildcard.show}"))) - else - Right(snapshots) - } - .map(indices => RuleDefinition.create(new SnapshotsRule(SnapshotsRule.Settings(indices)))) - .decoder - } -} - -object RepositoriesRuleDecoder - extends RuleBaseDecoderWithoutAssociatedFields[RepositoriesRule] { - - override protected def decoder: Decoder[RuleDefinition[RepositoriesRule]] = { - DecoderHelpers - .decodeStringLikeOrNonEmptySet[RuntimeMultiResolvableVariable[RepositoryName]] - .toSyncDecoder - .emapE { repositories => - if (checkIfAlreadyResolvedRepositoryVariableContains(repositories, RepositoryName.all)) - Left(RulesLevelCreationError(Message(s"Setting up a rule (${RepositoriesRule.Name.show}) that matches all the values is redundant - repository ${RepositoryName.all.show}"))) - else if (checkIfAlreadyResolvedRepositoryVariableContains(repositories, RepositoryName.wildcard)) - Left(RulesLevelCreationError(Message(s"Setting up a rule (${RepositoriesRule.Name.show}) that matches all the values is redundant - repository ${RepositoryName.wildcard.show}"))) - else - Right(repositories) - } - .map(repositories => RuleDefinition.create(new RepositoriesRule(RepositoriesRule.Settings(repositories)))) - .decoder - } -} - -private object RepositoriesDecodersHelper { - private implicit val indexNameConvertible: Convertible[RepositoryName] = new Convertible[RepositoryName] { - override def convert: String => Either[Convertible.ConvertError, RepositoryName] = str => - RepositoryName.from(str) match { - case Some(value) => Right(value) - case None => Left(Convertible.ConvertError("Repository name cannot be empty")) - } - } - implicit val repositoryValueDecoder: Decoder[RuntimeMultiResolvableVariable[RepositoryName]] = - DecoderHelpers - .decodeStringLikeNonEmpty - .toSyncDecoder - .emapE { str => - RuntimeResolvableVariableCreator - .createMultiResolvableVariableFrom[RepositoryName](str) - .left.map(error => RulesLevelCreationError(Message(error.show))) - } - .decoder - - private[rules] def checkIfAlreadyResolvedRepositoryVariableContains(repositoriesVars: NonEmptySet[RuntimeMultiResolvableVariable[RepositoryName]], - repository: RepositoryName): Boolean = { - repositoriesVars - .find { - case AlreadyResolved(repositories) => repositories.contains_(repository) - case ToBeResolved(_) => false - } - .isDefined - } -} - -private object SnapshotDecodersHelper { - private implicit val snapshotNameConvertible: Convertible[SnapshotName] = new Convertible[SnapshotName] { - override def convert: String => Either[Convertible.ConvertError, SnapshotName] = str => - SnapshotName.from(str) match { - case Some(value) => Right(value) - case None => Left(Convertible.ConvertError("Snapshot name cannot be empty")) - } - } - implicit val snapshotNameValueDecoder: Decoder[RuntimeMultiResolvableVariable[SnapshotName]] = - DecoderHelpers - .decodeStringLikeNonEmpty - .toSyncDecoder - .emapE { str => - RuntimeResolvableVariableCreator - .createMultiResolvableVariableFrom[SnapshotName](str) - .left.map(error => RulesLevelCreationError(Message(error.show))) - } - .decoder - - private[rules] def checkIfAlreadyResolvedSnapshotVariableContains(snapshotVars: NonEmptySet[RuntimeMultiResolvableVariable[SnapshotName]], - snapshot: SnapshotName): Boolean = { - snapshotVars - .find { - case AlreadyResolved(snapshots) => snapshots.contains_(snapshot) - case ToBeResolved(_) => false - } - .isDefined - } -} - private object IndicesDecodersHelper { private implicit val indexNameConvertible: Convertible[ClusterIndexName] = new Convertible[ClusterIndexName] { override def convert: String => Either[Convertible.ConvertError, ClusterIndexName] = str => diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/RepositoriesRuleDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/RepositoriesRuleDecoder.scala new file mode 100644 index 0000000000..ce99d6482f --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/RepositoriesRuleDecoder.scala @@ -0,0 +1,84 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.factory.decoders.rules + +import cats.data.NonEmptySet +import cats.implicits._ +import io.circe.Decoder +import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition +import tech.beshu.ror.accesscontrol.blocks.rules.RepositoriesRule +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable.{AlreadyResolved, ToBeResolved} +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeResolvableVariable.Convertible +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.{RuntimeMultiResolvableVariable, RuntimeResolvableVariableCreator} +import tech.beshu.ror.accesscontrol.domain.RepositoryName +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.accesscontrol.factory.decoders.rules.RepositoriesDecodersHelper._ +import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields +import tech.beshu.ror.accesscontrol.orders._ +import tech.beshu.ror.accesscontrol.show.logs._ +import tech.beshu.ror.accesscontrol.utils.CirceOps.{DecoderHelpers, _} + +object RepositoriesRuleDecoder + extends RuleBaseDecoderWithoutAssociatedFields[RepositoriesRule] { + + override protected def decoder: Decoder[RuleDefinition[RepositoriesRule]] = { + DecoderHelpers + .decodeStringLikeOrNonEmptySet[RuntimeMultiResolvableVariable[RepositoryName]] + .toSyncDecoder + .emapE { repositories => + if (checkIfAlreadyResolvedRepositoryVariableContains(repositories, RepositoryName.all)) + Left(RulesLevelCreationError(Message(s"Setting up a rule (${RepositoriesRule.Name.show}) that matches all the values is redundant - repository ${RepositoryName.all.show}"))) + else if (checkIfAlreadyResolvedRepositoryVariableContains(repositories, RepositoryName.wildcard)) + Left(RulesLevelCreationError(Message(s"Setting up a rule (${RepositoriesRule.Name.show}) that matches all the values is redundant - repository ${RepositoryName.wildcard.show}"))) + else + Right(repositories) + } + .map(repositories => RuleDefinition.create(new RepositoriesRule(RepositoriesRule.Settings(repositories)))) + .decoder + } +} + +private object RepositoriesDecodersHelper { + private implicit val indexNameConvertible: Convertible[RepositoryName] = new Convertible[RepositoryName] { + override def convert: String => Either[Convertible.ConvertError, RepositoryName] = str => + RepositoryName.from(str) match { + case Some(value) => Right(value) + case None => Left(Convertible.ConvertError("Repository name cannot be empty")) + } + } + implicit val repositoryValueDecoder: Decoder[RuntimeMultiResolvableVariable[RepositoryName]] = + DecoderHelpers + .decodeStringLikeNonEmpty + .toSyncDecoder + .emapE { str => + RuntimeResolvableVariableCreator + .createMultiResolvableVariableFrom[RepositoryName](str) + .left.map(error => RulesLevelCreationError(Message(error.show))) + } + .decoder + + private[rules] def checkIfAlreadyResolvedRepositoryVariableContains(repositoriesVars: NonEmptySet[RuntimeMultiResolvableVariable[RepositoryName]], + repository: RepositoryName): Boolean = { + repositoriesVars + .find { + case AlreadyResolved(repositories) => repositories.contains_(repository) + case ToBeResolved(_) => false + } + .isDefined + } +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/SnapshotsRuleDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/SnapshotsRuleDecoder.scala new file mode 100644 index 0000000000..f7f296bccd --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/SnapshotsRuleDecoder.scala @@ -0,0 +1,85 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.factory.decoders.rules + +import cats.data.NonEmptySet +import cats.implicits._ +import io.circe.Decoder +import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition +import tech.beshu.ror.accesscontrol.blocks.rules.SnapshotsRule +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.{RuntimeMultiResolvableVariable, RuntimeResolvableVariableCreator} +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable.{AlreadyResolved, ToBeResolved} +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeResolvableVariable.Convertible +import tech.beshu.ror.accesscontrol.domain.SnapshotName +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields +import tech.beshu.ror.accesscontrol.factory.decoders.rules.SnapshotDecodersHelper._ +import tech.beshu.ror.accesscontrol.orders._ +import tech.beshu.ror.accesscontrol.show.logs._ +import tech.beshu.ror.accesscontrol.utils.CirceOps.{DecoderHelpers, _} + + +object SnapshotsRuleDecoder + extends RuleBaseDecoderWithoutAssociatedFields[SnapshotsRule] { + + override protected def decoder: Decoder[RuleDefinition[SnapshotsRule]] = { + DecoderHelpers + .decodeStringLikeOrNonEmptySet[RuntimeMultiResolvableVariable[SnapshotName]] + .toSyncDecoder + .emapE { snapshots => + if (SnapshotDecodersHelper.checkIfAlreadyResolvedSnapshotVariableContains(snapshots, SnapshotName.All)) + Left(RulesLevelCreationError(Message(s"Setting up a rule (${SnapshotsRule.Name.show}) that matches all the values is redundant - snapshot ${SnapshotName.all.show}"))) + else if (SnapshotDecodersHelper.checkIfAlreadyResolvedSnapshotVariableContains(snapshots, SnapshotName.Wildcard)) + Left(RulesLevelCreationError(Message(s"Setting up a rule (${SnapshotsRule.Name.show}) that matches all the values is redundant - snapshot ${SnapshotName.wildcard.show}"))) + else + Right(snapshots) + } + .map(indices => RuleDefinition.create(new SnapshotsRule(SnapshotsRule.Settings(indices)))) + .decoder + } +} + +private object SnapshotDecodersHelper { + private implicit val snapshotNameConvertible: Convertible[SnapshotName] = new Convertible[SnapshotName] { + override def convert: String => Either[Convertible.ConvertError, SnapshotName] = str => + SnapshotName.from(str) match { + case Some(value) => Right(value) + case None => Left(Convertible.ConvertError("Snapshot name cannot be empty")) + } + } + implicit val snapshotNameValueDecoder: Decoder[RuntimeMultiResolvableVariable[SnapshotName]] = + DecoderHelpers + .decodeStringLikeNonEmpty + .toSyncDecoder + .emapE { str => + RuntimeResolvableVariableCreator + .createMultiResolvableVariableFrom[SnapshotName](str) + .left.map(error => RulesLevelCreationError(Message(error.show))) + } + .decoder + + private[rules] def checkIfAlreadyResolvedSnapshotVariableContains(snapshotVars: NonEmptySet[RuntimeMultiResolvableVariable[SnapshotName]], + snapshot: SnapshotName): Boolean = { + snapshotVars + .find { + case AlreadyResolved(snapshots) => snapshots.contains_(snapshot) + case ToBeResolved(_) => false + } + .isDefined + } +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/matchers/ZeroKnowledgeDataStreamsFilterScalaAdapter.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/matchers/ZeroKnowledgeDataStreamsFilterScalaAdapter.scala new file mode 100644 index 0000000000..34bac69793 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/matchers/ZeroKnowledgeDataStreamsFilterScalaAdapter.scala @@ -0,0 +1,48 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.matchers + +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.accesscontrol.matchers.ZeroKnowledgeDataStreamsFilterScalaAdapter.CheckResult +import tech.beshu.ror.utils.ZeroKnowledgeIndexFilter +import scala.collection.JavaConverters._ + +class ZeroKnowledgeDataStreamsFilterScalaAdapter(underlying: ZeroKnowledgeIndexFilter) { + def check(dataStreams: Set[DataStreamName], matcher: Matcher[DataStreamName]): CheckResult = { + val processedDataStreams: java.util.Set[String] = scala.collection.mutable.Set.empty[String].asJava + val result = underlying.alterIndicesIfNecessaryAndCheck( + dataStreams + .collect { + case DataStreamName.Pattern(v) => v.value + case DataStreamName.Full(v) => v.value + } + .asJava, + Matcher.asMatcherWithWildcards(matcher), + processedDataStreams.addAll _ + ) + if (result) CheckResult.Ok(processedDataStreams.asScala.flatMap(DataStreamName.fromString).toSet) + else CheckResult.Failed + } +} + +object ZeroKnowledgeDataStreamsFilterScalaAdapter { + sealed trait CheckResult + object CheckResult { + final case class Ok(processedDataStreams: Set[DataStreamName]) extends CheckResult + case object Failed extends CheckResult + } +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/matchers/ZeroKnowledgeMatchFilterScalaAdapter.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/matchers/ZeroKnowledgeMatchFilterScalaAdapter.scala index be4c318349..4a5062cf10 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/matchers/ZeroKnowledgeMatchFilterScalaAdapter.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/matchers/ZeroKnowledgeMatchFilterScalaAdapter.scala @@ -16,7 +16,7 @@ */ package tech.beshu.ror.accesscontrol.matchers -import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName, RepositoryName, SnapshotName} import tech.beshu.ror.accesscontrol.matchers.ZeroKnowledgeMatchFilterScalaAdapter.AlterResult import tech.beshu.ror.utils.ZeroKnowledgeMatchFilter diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/ops.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/ops.scala index efbcbddfac..7e34f7a6e9 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/ops.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/ops.scala @@ -124,6 +124,12 @@ object orders { case SnapshotName.All => "_all" case SnapshotName.Wildcard => "*" } + implicit val dataStreamOrder: Order[DataStreamName] = Order.by { + case DataStreamName.Full(value) => value.value + case DataStreamName.Pattern(value) => value.value + case DataStreamName.All => "_all" + case DataStreamName.Wildcard => "*" + } implicit def accessOrder[T: Order]: Order[AccessRequirement[T]] = Order.from { case (MustBeAbsent(v1), MustBeAbsent(v2)) => v1.compare(v2) diff --git a/core/src/test/scala/tech/beshu/ror/mocks/MockRequestContext.scala b/core/src/test/scala/tech/beshu/ror/mocks/MockRequestContext.scala index 2763bba8ec..0c1050a5a2 100644 --- a/core/src/test/scala/tech/beshu/ror/mocks/MockRequestContext.scala +++ b/core/src/test/scala/tech/beshu/ror/mocks/MockRequestContext.scala @@ -17,12 +17,12 @@ package tech.beshu.ror.mocks import java.time.{Clock, Instant} - import com.softwaremill.sttp.Method import eu.timepit.refined.auto._ import monix.eval.Task import squants.information.{Bytes, Information} import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext.Indices import tech.beshu.ror.accesscontrol.blocks.BlockContext._ import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata @@ -39,7 +39,7 @@ object MockRequestContext { def indices(implicit clock: Clock = Clock.systemUTC()): MockGeneralIndexRequestContext = MockGeneralIndexRequestContext(timestamp = clock.instant(), filteredIndices = Set.empty, allAllowedIndices = Set.empty) - def filterableMulti(implicit clock: Clock = Clock.systemUTC()): MockFilterableMultiRequestContext = + def filterableMulti(implicit clock: Clock = Clock.systemUTC()): MockFilterableMultiRequestContext = MockFilterableMultiRequestContext(timestamp = clock.instant(), indexPacks = List.empty, filter = None, fieldLevelSecurity = None, requestFieldsUsage = RequestFieldsUsage.CannotExtractFields) def nonIndices(implicit clock: Clock = Clock.systemUTC()): MockGeneralNonIndexRequestContext = @@ -54,6 +54,9 @@ object MockRequestContext { def snapshots(implicit clock: Clock = Clock.systemUTC()): MockSnapshotsRequestContext = MockSnapshotsRequestContext(timestamp = clock.instant(), snapshots = Set.empty) + def dataStreams(implicit clock: Clock = Clock.systemUTC()): MockDataStreamsRequestContext = + MockDataStreamsRequestContext(timestamp = clock.instant(), dataStreams = Set.empty) + def metadata(implicit clock: Clock = Clock.systemUTC()): MockUserMetadataRequestContext = MockUserMetadataRequestContext(timestamp = clock.instant()) @@ -252,6 +255,35 @@ final case class MockSnapshotsRequestContext(override val timestamp: Instant, ) } +final case class MockDataStreamsRequestContext(override val timestamp: Instant, + override val taskId: Long = 0L, + override val id: RequestContext.Id = RequestContext.Id("mock"), + override val `type`: Type = Type("default-type"), + override val action: Action = defaultAction, + override val headers: Set[Header] = Set.empty, + override val remoteAddress: Option[Address] = Address.from("localhost"), + override val localAddress: Address = Address.from("localhost").get, + override val method: Method = Method("GET"), + override val uriPath: UriPath = UriPath.currentUserMetadataPath, + override val contentLength: Information = Bytes(0), + override val content: String = "", + override val indexAttributes: Set[IndexAttribute] = Set.empty, + override val allIndicesAndAliases: Set[FullLocalIndexWithAliases] = Set.empty, + override val allTemplates: Set[Template] = Set.empty, + override val allSnapshots: Map[RepositoryName.Full, Set[SnapshotName.Full]] = Map.empty, + override val allRemoteIndicesAndAliases: Task[Set[FullRemoteIndexWithAliases]] = Task.now(Set.empty), + override val isCompositeRequest: Boolean = false, + override val isReadOnlyRequest: Boolean = true, + override val isAllowedForDLS: Boolean = true, + dataStreams: Set[DataStreamName]) + extends RequestContext { + override type BLOCK_CONTEXT = DataStreamRequestBlockContext + + override def initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + this, UserMetadata.from(this), Set.empty, List.empty, dataStreams, BackingIndices.IndicesNotInvolved + ) +} + final case class MockUserMetadataRequestContext(override val timestamp: Instant, override val taskId: Long = 0L, override val id: RequestContext.Id = RequestContext.Id("mock"), diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/DataStreamsRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/DataStreamsRuleTests.scala new file mode 100644 index 0000000000..1ea0146e39 --- /dev/null +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/DataStreamsRuleTests.scala @@ -0,0 +1,195 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.unit.acl.blocks.rules + +import cats.data.NonEmptySet +import monix.execution.Scheduler.Implicits.global +import org.scalatest.Inside +import org.scalatest.matchers.should.Matchers._ +import org.scalatest.wordspec.AnyWordSpec +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.blocks.rules.DataStreamsRule +import tech.beshu.ror.accesscontrol.blocks.rules.base.Rule.RuleResult.{Fulfilled, Rejected} +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable.AlreadyResolved +import tech.beshu.ror.accesscontrol.domain.{Action, DataStreamName} +import tech.beshu.ror.accesscontrol.orders._ +import tech.beshu.ror.mocks.MockRequestContext +import tech.beshu.ror.utils.TestsUtils._ + +import scala.concurrent.duration._ +import scala.language.postfixOps + +class DataStreamsRuleTests extends AnyWordSpec with Inside { + + "A DataStreamsRule" should { + "match" when { + "allowed data streams set contains *" in { + assertMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.Wildcard.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("data_stream1").get) + ) { + blockContext => blockContext.dataStreams should be(Set(DataStreamName.fromString("data_stream1").get)) + } + } + "allowed data streams set contains _all" in { + assertMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.All.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("data_stream1").get) + ) { + blockContext => blockContext.dataStreams should be(Set(DataStreamName.fromString("data_stream1").get)) + } + } + "readonly request with configured simple data stream" in { + assertMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("public-asd").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get), + readonlyRequest = true + ) { + blockContext => blockContext.dataStreams should be(Set(DataStreamName.fromString("public-asd").get)) + } + } + "readonly request with configured data stream with wildcard" in { + assertMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("public-*").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get), + readonlyRequest = true + ) { + blockContext => blockContext.dataStreams should be(Set(DataStreamName.fromString("public-asd").get)) + } + } + "write request with configured simple data stream" in { + assertMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("public-asd").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get) + ) { + blockContext => blockContext.dataStreams should be(Set(DataStreamName.fromString("public-asd").get)) + } + } + "write request with configured data stream with wildcard" in { + assertMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("public-*").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get) + ) { + blockContext => blockContext.dataStreams should be(Set(DataStreamName.fromString("public-asd").get)) + } + } + "readonly request with configured several data streams and several data streams in request" in { + assertMatchRule( + configuredDataStreams = NonEmptySet.of( + AlreadyResolved(DataStreamName.fromString("public-*").get.nel), + AlreadyResolved(DataStreamName.fromString("n").get.nel) + ), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get, DataStreamName.fromString("q").get), + readonlyRequest = true + ) { + blockContext => blockContext.dataStreams should be(Set(DataStreamName.fromString("public-asd").get)) + } + } + } + "not match" when { + "request is read only" in { + assertNotMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("x-*").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get), + readonlyRequest = true + ) + } + "write request with no match" in { + assertNotMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("public-*").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("x_public-asd").get) + ) + } + "write request with configured several data streams and several data streams in request" in { + assertNotMatchRule( + configuredDataStreams = NonEmptySet.of( + AlreadyResolved(DataStreamName.fromString("public-*").get.nel), + AlreadyResolved(DataStreamName.fromString("n").get.nel) + ), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get, DataStreamName.fromString("q").get) + ) + } + "write request forbid" in { + assertNotMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("x-*").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get, DataStreamName.fromString("q").get) + ) + } + "read request forbid" in { + assertNotMatchRule( + configuredDataStreams = NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("x-*").get.nel)), + requestAction = Action("indices:admin/data_stream/get"), + requestDataStreams = Set(DataStreamName.fromString("public-asd").get, DataStreamName.fromString("q").get), + readonlyRequest = true + ) + } + } + } + + private def assertMatchRule(configuredDataStreams: NonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]], + requestAction: Action, + requestDataStreams: Set[DataStreamName], + readonlyRequest: Boolean = false) + (blockContextAssertion: DataStreamRequestBlockContext => Unit): Unit = + assertRule(configuredDataStreams, requestAction, requestDataStreams, readonlyRequest, Some(blockContextAssertion)) + + private def assertNotMatchRule(configuredDataStreams: NonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]], + requestAction: Action, + requestDataStreams: Set[DataStreamName], + readonlyRequest: Boolean = false): Unit = + assertRule(configuredDataStreams, requestAction, requestDataStreams, readonlyRequest, blockContextAssertion = None) + + private def assertRule(configuredDataStreams: NonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]], + requestAction: Action, + requestDataStreams: Set[DataStreamName], + readonlyRequest: Boolean, + blockContextAssertion: Option[DataStreamRequestBlockContext => Unit]) = { + val rule = new DataStreamsRule(DataStreamsRule.Settings(configuredDataStreams)) + val requestContext = MockRequestContext.dataStreams.copy( + dataStreams = requestDataStreams, + action = requestAction, + isReadOnlyRequest = readonlyRequest + ) + val blockContext = DataStreamRequestBlockContext( + requestContext, UserMetadata.empty, Set.empty, List.empty, requestDataStreams, BackingIndices.IndicesNotInvolved + ) + val result = rule.check(blockContext).runSyncUnsafe(1 second) + blockContextAssertion match { + case Some(assertOutputBlockContext) => + inside(result) { case Fulfilled(outBlockContext) => + assertOutputBlockContext(outBlockContext) + } + case None => + result should be(Rejected()) + } + } + +} diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/DataStreamsRuleSettingsTest.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/DataStreamsRuleSettingsTest.scala new file mode 100644 index 0000000000..08d46960a6 --- /dev/null +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/DataStreamsRuleSettingsTest.scala @@ -0,0 +1,198 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.unit.acl.factory.decoders + +import cats.data.NonEmptySet +import org.scalatest.matchers.should.Matchers._ +import tech.beshu.ror.accesscontrol.blocks.rules.DataStreamsRule +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeMultiResolvableVariable.{AlreadyResolved, ToBeResolved} +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.accesscontrol.orders._ +import tech.beshu.ror.utils.TestsUtils._ + +class DataStreamsRuleSettingsTest extends BaseRuleSettingsDecoderTest[DataStreamsRule] { + + "A DataStreamsRule" should { + "be able to be loaded from config" when { + "one data stream is defined" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | data_streams: data_stream1 + | + |""".stripMargin, + assertion = rule => { + val indices: NonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]] = + NonEmptySet.one(AlreadyResolved(DataStreamName.fromString("data_stream1").get.nel)) + rule.settings.allowedDataStreams should be(indices) + } + ) + } + "data stream is defined with variable" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | auth_key: user:pass + | data_streams: "data_stream_@{user}" + | + |""".stripMargin, + assertion = rule => { + rule.settings.allowedDataStreams.length should be(1) + rule.settings.allowedDataStreams.head shouldBe a[ToBeResolved[_]] + } + ) + } + "two data streams are defined" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | data_streams: [data_stream1, data_stream2] + | + |""".stripMargin, + assertion = rule => { + val indices: NonEmptySet[RuntimeMultiResolvableVariable[DataStreamName]] = NonEmptySet.of( + AlreadyResolved(DataStreamName.fromString("data_stream1").get.nel), + AlreadyResolved(DataStreamName.fromString("data_stream2").get.nel) + ) + rule.settings.allowedDataStreams should be(indices) + } + ) + } + "two data streams are defined, second one with variable" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | auth_key: user:pass + | data_streams: [data_stream1, "data_stream_@{user}"] + | + |""".stripMargin, + assertion = rule => { + rule.settings.allowedDataStreams.length == 2 + + rule.settings.allowedDataStreams.head should be(AlreadyResolved(DataStreamName.fromString("data_stream1").get.nel)) + rule.settings.allowedDataStreams.tail.head shouldBe a[ToBeResolved[_]] + } + ) + } + } + "not be able to be loaded from config" when { + "no data stream is defined" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | data_streams: + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(MalformedValue( + """data_streams: null + |""".stripMargin))) + } + ) + } + "data stream name contains upper case characters" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | data_streams: Data_Stream1 + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message( + "Data stream name cannot contain the upper case characters" + ))) + } + ) + } + "there is '_all' data stream defined" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | data_streams: [data_stream1, _all] + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message( + "Setting up a rule (data_streams) that matches all the values is redundant - data stream *" + ))) + } + ) + } + "there is '*' data stream defined" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | data_streams: ["data_stream1", "*", "data_stream2"] + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message( + "Setting up a rule (data_streams) that matches all the values is redundant - data stream *" + ))) + } + ) + } + } + } +} diff --git a/core/src/test/scala/tech/beshu/ror/utils/TestsUtils.scala b/core/src/test/scala/tech/beshu/ror/utils/TestsUtils.scala index c522eda9bb..d579d98e06 100644 --- a/core/src/test/scala/tech/beshu/ror/utils/TestsUtils.scala +++ b/core/src/test/scala/tech/beshu/ror/utils/TestsUtils.scala @@ -26,7 +26,7 @@ import io.circe.ParsingFailure import io.jsonwebtoken.JwtBuilder import org.scalatest.matchers.should.Matchers._ import tech.beshu.ror.RequestId -import tech.beshu.ror.accesscontrol.blocks.BlockContext.{AliasRequestBlockContext, CurrentUserMetadataRequestBlockContext, FilterableMultiRequestBlockContext, FilterableRequestBlockContext, GeneralIndexRequestBlockContext, GeneralNonIndexRequestBlockContext, MultiIndexRequestBlockContext, RepositoryRequestBlockContext, RorApiRequestBlockContext, SnapshotRequestBlockContext, TemplateRequestBlockContext} +import tech.beshu.ror.accesscontrol.blocks.BlockContext._ import tech.beshu.ror.accesscontrol.blocks.definitions.ImpersonatorDef.ImpersonatedUsers import tech.beshu.ror.accesscontrol.blocks.definitions.UserDef.GroupMappings import tech.beshu.ror.accesscontrol.blocks.definitions.ldap.LdapService @@ -215,6 +215,7 @@ object TestsUtils { aliases: Set[ClusterIndexName] = Set.empty, repositories: Set[RepositoryName] = Set.empty, snapshots: Set[SnapshotName] = Set.empty, + dataStreams: Set[DataStreamName] = Set.empty, templates: Set[TemplateOperation] = Set.empty) (blockContext: BlockContext): Unit = { blockContext.userMetadata.loggedUser should be(loggedUser) @@ -231,6 +232,8 @@ object TestsUtils { case _: CurrentUserMetadataRequestBlockContext => case _: RorApiRequestBlockContext => case _: GeneralNonIndexRequestBlockContext => + case bc: DataStreamRequestBlockContext => + bc.dataStreams should be(dataStreams) case _: RorApiRequestBlockContext => case bc: RepositoryRequestBlockContext => bc.repositories should be(repositories) diff --git a/es60x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es60x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index 90fbc951f8..2252484ded 100644 --- a/es60x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es60x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es61x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es61x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index 90fbc951f8..2252484ded 100644 --- a/es61x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es61x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es62x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es62x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index 90fbc951f8..2252484ded 100644 --- a/es62x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es62x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es63x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es63x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index 1d76d75395..aefd87294c 100644 --- a/es63x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es63x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es65x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es65x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index 1d76d75395..aefd87294c 100644 --- a/es65x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es65x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es66x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es66x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index 1d76d75395..aefd87294c 100644 --- a/es66x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es66x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es67x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es67x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es67x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es67x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es70x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es70x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es70x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es70x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 4f7d9febed..623b2dfc29 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -68,6 +68,7 @@ import tech.beshu.ror.es.actions.RorActionRequest import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventRequest import tech.beshu.ror.es.actions.rrmetadata.RRUserMetadataRequest import tech.beshu.ror.es.handler.AclAwareRequestFilter._ +import tech.beshu.ror.es.handler.request.ActionRequestOps._ import tech.beshu.ror.es.handler.request.RestRequestOps._ import tech.beshu.ror.es.handler.request.context.types._ import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext @@ -202,7 +203,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new RolloverEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ResolveIndexAction.Request => regularRequestHandler.handle(new ResolveIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) - case request: IndicesRequest.Replaceable => + case request: IndicesRequest.Replaceable if request.notDataStreamRelated => regularRequestHandler.handle(new IndicesReplaceableEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ReindexRequest => regularRequestHandler.handle(new ReindexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala new file mode 100644 index 0000000000..09fbbc8857 --- /dev/null +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala @@ -0,0 +1,38 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request + +import org.elasticsearch.action.ActionRequest +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext + +object ActionRequestOps { + + implicit class RequestOps(val actionRequest: ActionRequest) extends AnyVal { + + def notDataStreamRelated: Boolean = { + !RequestOps + .dataStreamRequests + .contains(actionRequest.getClass.getCanonicalName) + } + } + + private object RequestOps { + private val dataStreamRequests: Set[String] = + ReflectionBasedDataStreamsEsRequestContext.supportedActionRequests.map(_.value) + } + +} diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index cfa253d507..6ff5ce4a0e 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala deleted file mode 100644 index 17c8c7d5b0..0000000000 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.common.util.set.Sets -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext -import tech.beshu.ror.utils.ReflecUtils - -import scala.collection.JavaConverters._ - -private[datastreams] abstract class BaseReadDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (tryUpdate(actionRequest, filteredIndices)) Modified - else { - logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") - ShouldBeInterrupted - } - } - - private def tryUpdate(actionRequest: R, indices: NonEmptyList[ClusterIndexName]) = { - // Optimistic reflection attempt - ReflecUtils.setIndices( - actionRequest, - Sets.newHashSet(setIndicesMethodName), - indices.toList.map(_.stringify).toSet.asJava - ) - } - - protected def setIndicesMethodName: String -} diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala deleted file mode 100644 index 939595ea5a..0000000000 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext - -private[datastreams] abstract class BaseWriteDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (indices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } - } -} diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala index a14811e9a8..3adf6011ad 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala @@ -18,33 +18,45 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class CreateDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = dataStreams + + override def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -private[datastreams] object CreateDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +private[datastreams] object CreateDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala index 98ad908357..537241eaad 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala @@ -16,36 +16,60 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DataStreamsStatsEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "indices", + dataStreams = blockContext.dataStreams + ) + } - override protected def setIndicesMethodName: String = "indices" } -object DataStreamsStatsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DataStreamsStatsEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala index 0a22f6be32..b12dddc61f 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala @@ -16,36 +16,59 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DeleteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override protected def setIndicesMethodName: String = "indices" + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } } -object DeleteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DeleteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala index ef6c28966e..27d887adc6 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala @@ -16,36 +16,125 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import org.elasticsearch.action.ActionRequest +import cats.implicits.toShow +import monix.eval.Task +import org.elasticsearch.action.{ActionRequest, ActionResponse} import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils.{invokeMethod, setField} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.collection.JavaConverters._ +import scala.util.Try private[datastreams] class GetDataStreamEsRequestContext(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, + allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.UpdateResponse { + case r: ActionResponse if isGetDataStreamActionResponse(r) => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(_, allAllowedIndices) => + Task.now(updateActionResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + + } + + private def modifyActionRequest(blockContext: BlockContext.DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } + + private def isGetDataStreamActionResponse(r: ActionResponse) = { + r.getClass.getCanonicalName == "org.elasticsearch.xpack.core.action.GetDataStreamAction.Response" + } - override protected def setIndicesMethodName: String = "indices" + + private def updateActionResponse(response: ActionResponse, + allAllowedIndices: Set[ClusterIndexName]): ActionResponse = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredDataStreams = + invokeMethod(response, response.getClass, "getDataStreams") + .asInstanceOf[java.util.List[Object]] + .asScala + .filter { dataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + + setField(response, response.getClass, "dataStreams", filteredDataStreams.asJava) + response + } + + private def backingIndiesMatchesAllowedIndices(info: Object, allowedIndicesMatcher: Matcher[ClusterIndexName]): Boolean = { + val dataStreamIndices = indicesFromDataStreamInfo(info).get + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty + } + + private def indicesFromDataStreamInfo(info: Object): Try[Set[ClusterIndexName]] = { + for { + dataStream <- Try(invokeMethod(info, info.getClass, "getDataStream")) + backingIndices <- Try { + invokeMethod(dataStream, dataStream.getClass, "getIndices") + .asInstanceOf[java.util.List[Object]] + .asSafeList + } + indices <- Try { + backingIndices + .flatMap(backingIndex => Option(invokeMethod(backingIndex, backingIndex.getClass, "getName").asInstanceOf[String])) + .flatMap(ClusterIndexName.fromString) + .toSet + } + } yield indices + + } } -object GetDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object GetDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.GetDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala index 86c8e30a13..5b080621cd 100644 --- a/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala +++ b/es710x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala @@ -16,42 +16,89 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList import cats.implicits._ import org.elasticsearch.action.ActionRequest -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import org.elasticsearch.common.util.set.Sets +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult.{Matched, NotMatched} -import tech.beshu.ror.es.handler.request.context.types.{BaseIndicesEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils import tech.beshu.ror.utils.ReflecUtils.extractStringArrayFromPrivateMethod import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.collection.JavaConverters._ object ReflectionBasedDataStreamsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[BaseIndicesEsRequestContext[ActionRequest]] = { - CreateDataStreamEsRequestContext.unapply(arg) - .orElse(DataStreamsStatsEsRequestContext.unapply(arg)) - .orElse(DeleteDataStreamEsRequestContext.unapply(arg)) - .orElse(GetDataStreamEsRequestContext.unapply(arg)) + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] = { + esContextCreators + .toStream + .flatMap(_.unapply(arg)) + .headOption } - private[datastreams] def tryMatchActionRequest(actionRequest: ActionRequest, - expectedClassCanonicalName: String, - getIndicesMethodName: String): MatchResult = { - Option(actionRequest.getClass.getCanonicalName) - .find(_ == expectedClassCanonicalName) - .flatMap { _ => - NonEmptyList - .fromList(extractStringArrayFromPrivateMethod(getIndicesMethodName, actionRequest).asSafeList) - .map(_.toList.toSet.flatMap(ClusterIndexName.fromString)) - .map(Matched.apply) - } - .getOrElse(NotMatched) + val supportedActionRequests: Set[ClassCanonicalName] = esContextCreators.map(_.actionRequestClass).toSet + + private lazy val esContextCreators: UniqueNonEmptyList[ReflectionBasedDataStreamsEsContextCreator] = UniqueNonEmptyList.of( + CreateDataStreamEsRequestContext, + DataStreamsStatsEsRequestContext, + DeleteDataStreamEsRequestContext, + GetDataStreamEsRequestContext, + ) + + private[datastreams] def tryUpdateDataStreams[R <: ActionRequest](actionRequest: R, + dataStreamsFieldName: String, + dataStreams: Set[DataStreamName]): Boolean = { + // Optimistic reflection attempt + ReflecUtils.setIndices( + actionRequest, + Sets.newHashSet(dataStreamsFieldName), + dataStreams.toList.map(DataStreamName.toString).toSet.asJava + ) } - private[datastreams] sealed trait MatchResult + + private[datastreams] sealed trait MatchResult[+A] + private[datastreams] object MatchResult { - final case class Matched(extractedIndices: Set[ClusterIndexName]) extends MatchResult - object NotMatched extends MatchResult + final case class Matched[A](extracted: Set[A]) extends MatchResult[A] + + object NotMatched extends MatchResult[Nothing] + } + + final case class ClassCanonicalName(value: String) extends AnyVal + + private[datastreams] trait ReflectionBasedDataStreamsEsContextCreator { + def actionRequestClass: ClassCanonicalName + + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] + + protected def tryMatchActionRequestWithDataStreams(actionRequest: ActionRequest, + getDataStreamsMethodName: String): MatchResult[DataStreamName] = { + tryMatchActionRequest[DataStreamName]( + actionRequest = actionRequest, + getPropsMethodName = getDataStreamsMethodName, + toDomain = DataStreamName.fromString + ) + } + + private def tryMatchActionRequest[A](actionRequest: ActionRequest, + getPropsMethodName: String, + toDomain: String => Option[A]): MatchResult[A] = { + Option(actionRequest.getClass.getCanonicalName) + .find(_ === actionRequestClass.value) + .map { _ => + Matched.apply[A] { + extractStringArrayFromPrivateMethod(getPropsMethodName, actionRequest) + .asSafeList + .toSet + .flatMap((value: String) => toDomain(value)) + } + } + .getOrElse(NotMatched) + } + } } \ No newline at end of file diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 4f7d9febed..623b2dfc29 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -68,6 +68,7 @@ import tech.beshu.ror.es.actions.RorActionRequest import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventRequest import tech.beshu.ror.es.actions.rrmetadata.RRUserMetadataRequest import tech.beshu.ror.es.handler.AclAwareRequestFilter._ +import tech.beshu.ror.es.handler.request.ActionRequestOps._ import tech.beshu.ror.es.handler.request.RestRequestOps._ import tech.beshu.ror.es.handler.request.context.types._ import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext @@ -202,7 +203,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new RolloverEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ResolveIndexAction.Request => regularRequestHandler.handle(new ResolveIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) - case request: IndicesRequest.Replaceable => + case request: IndicesRequest.Replaceable if request.notDataStreamRelated => regularRequestHandler.handle(new IndicesReplaceableEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ReindexRequest => regularRequestHandler.handle(new ReindexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala new file mode 100644 index 0000000000..09fbbc8857 --- /dev/null +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala @@ -0,0 +1,38 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request + +import org.elasticsearch.action.ActionRequest +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext + +object ActionRequestOps { + + implicit class RequestOps(val actionRequest: ActionRequest) extends AnyVal { + + def notDataStreamRelated: Boolean = { + !RequestOps + .dataStreamRequests + .contains(actionRequest.getClass.getCanonicalName) + } + } + + private object RequestOps { + private val dataStreamRequests: Set[String] = + ReflectionBasedDataStreamsEsRequestContext.supportedActionRequests.map(_.value) + } + +} diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index cfa253d507..6ff5ce4a0e 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala deleted file mode 100644 index 17ec1eea97..0000000000 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.common.util.set.Sets -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext -import tech.beshu.ror.utils.ReflecUtils - -import scala.collection.JavaConverters._ - -private[datastreams] abstract class BaseReadDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (tryUpdate(actionRequest, filteredIndices)) Modified - else { - logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") - ShouldBeInterrupted - } - } - - private def tryUpdate(actionRequest: R, indices: NonEmptyList[ClusterIndexName]) = { - // Optimistic reflection attempt - ReflecUtils.setIndices( - actionRequest, - Sets.newHashSet(setIndicesMethodName), - indices.toList.map(_.stringify).toSet.asJava - ) - } - - protected def setIndicesMethodName: String -} \ No newline at end of file diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala deleted file mode 100644 index 939595ea5a..0000000000 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext - -private[datastreams] abstract class BaseWriteDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (indices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } - } -} diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala index a14811e9a8..3adf6011ad 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala @@ -18,33 +18,45 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class CreateDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = dataStreams + + override def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -private[datastreams] object CreateDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +private[datastreams] object CreateDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala index 98ad908357..537241eaad 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala @@ -16,36 +16,60 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DataStreamsStatsEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "indices", + dataStreams = blockContext.dataStreams + ) + } - override protected def setIndicesMethodName: String = "indices" } -object DataStreamsStatsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DataStreamsStatsEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala index 0a22f6be32..b12dddc61f 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala @@ -16,36 +16,59 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DeleteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override protected def setIndicesMethodName: String = "indices" + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } } -object DeleteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DeleteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala index ef6c28966e..27d887adc6 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala @@ -16,36 +16,125 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import org.elasticsearch.action.ActionRequest +import cats.implicits.toShow +import monix.eval.Task +import org.elasticsearch.action.{ActionRequest, ActionResponse} import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils.{invokeMethod, setField} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.collection.JavaConverters._ +import scala.util.Try private[datastreams] class GetDataStreamEsRequestContext(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, + allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.UpdateResponse { + case r: ActionResponse if isGetDataStreamActionResponse(r) => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(_, allAllowedIndices) => + Task.now(updateActionResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + + } + + private def modifyActionRequest(blockContext: BlockContext.DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } + + private def isGetDataStreamActionResponse(r: ActionResponse) = { + r.getClass.getCanonicalName == "org.elasticsearch.xpack.core.action.GetDataStreamAction.Response" + } - override protected def setIndicesMethodName: String = "indices" + + private def updateActionResponse(response: ActionResponse, + allAllowedIndices: Set[ClusterIndexName]): ActionResponse = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredDataStreams = + invokeMethod(response, response.getClass, "getDataStreams") + .asInstanceOf[java.util.List[Object]] + .asScala + .filter { dataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + + setField(response, response.getClass, "dataStreams", filteredDataStreams.asJava) + response + } + + private def backingIndiesMatchesAllowedIndices(info: Object, allowedIndicesMatcher: Matcher[ClusterIndexName]): Boolean = { + val dataStreamIndices = indicesFromDataStreamInfo(info).get + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty + } + + private def indicesFromDataStreamInfo(info: Object): Try[Set[ClusterIndexName]] = { + for { + dataStream <- Try(invokeMethod(info, info.getClass, "getDataStream")) + backingIndices <- Try { + invokeMethod(dataStream, dataStream.getClass, "getIndices") + .asInstanceOf[java.util.List[Object]] + .asSafeList + } + indices <- Try { + backingIndices + .flatMap(backingIndex => Option(invokeMethod(backingIndex, backingIndex.getClass, "getName").asInstanceOf[String])) + .flatMap(ClusterIndexName.fromString) + .toSet + } + } yield indices + + } } -object GetDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object GetDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.GetDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala index 8b1a5b432c..9b730f7210 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala @@ -18,33 +18,46 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} class MigrateToDataStreamEsRequestContext(actionRequest: ActionRequest, indices: Set[ClusterIndexName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = Set.empty + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved(indices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL + } } -object MigrateToDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { +object MigrateToDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { + tryMatchActionRequestWithIndices( + actionRequest = arg.esContext.actionRequest, + getIndicesMethodName = "indices" + ) match { case MatchResult.Matched(indices) => - Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala index bf1d00459a..86be1ed7c4 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala @@ -18,32 +18,43 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class PromoteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -object PromoteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object PromoteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala index deb036f7f0..39fab97548 100644 --- a/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala +++ b/es711x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala @@ -16,44 +16,100 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList import cats.implicits._ import org.elasticsearch.action.ActionRequest -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import org.elasticsearch.common.util.set.Sets +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult.{Matched, NotMatched} -import tech.beshu.ror.es.handler.request.context.types.{BaseIndicesEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils import tech.beshu.ror.utils.ReflecUtils.extractStringArrayFromPrivateMethod import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.collection.JavaConverters._ object ReflectionBasedDataStreamsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[BaseIndicesEsRequestContext[ActionRequest]] = { - CreateDataStreamEsRequestContext.unapply(arg) - .orElse(DataStreamsStatsEsRequestContext.unapply(arg)) - .orElse(DeleteDataStreamEsRequestContext.unapply(arg)) - .orElse(GetDataStreamEsRequestContext.unapply(arg)) - .orElse(MigrateToDataStreamEsRequestContext.unapply(arg)) - .orElse(PromoteDataStreamEsRequestContext.unapply(arg)) + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] = { + esContextCreators + .toStream + .flatMap(_.unapply(arg)) + .headOption } - private[datastreams] def tryMatchActionRequest(actionRequest: ActionRequest, - expectedClassCanonicalName: String, - getIndicesMethodName: String): MatchResult = { - Option(actionRequest.getClass.getCanonicalName) - .find(_ == expectedClassCanonicalName) - .flatMap { _ => - NonEmptyList - .fromList(extractStringArrayFromPrivateMethod(getIndicesMethodName, actionRequest).asSafeList) - .map(_.toList.toSet.flatMap(ClusterIndexName.fromString)) - .map(Matched.apply) - } - .getOrElse(NotMatched) + val supportedActionRequests: Set[ClassCanonicalName] = esContextCreators.map(_.actionRequestClass).toSet + + private lazy val esContextCreators: UniqueNonEmptyList[ReflectionBasedDataStreamsEsContextCreator] = UniqueNonEmptyList.of( + CreateDataStreamEsRequestContext, + DataStreamsStatsEsRequestContext, + DeleteDataStreamEsRequestContext, + GetDataStreamEsRequestContext, + MigrateToDataStreamEsRequestContext, + PromoteDataStreamEsRequestContext + ) + + private[datastreams] def tryUpdateDataStreams[R <: ActionRequest](actionRequest: R, + dataStreamsFieldName: String, + dataStreams: Set[DataStreamName]): Boolean = { + // Optimistic reflection attempt + ReflecUtils.setIndices( + actionRequest, + Sets.newHashSet(dataStreamsFieldName), + dataStreams.toList.map(DataStreamName.toString).toSet.asJava + ) } - private[datastreams] sealed trait MatchResult + + private[datastreams] sealed trait MatchResult[+A] + private[datastreams] object MatchResult { - final case class Matched(extractedIndices: Set[ClusterIndexName]) extends MatchResult - object NotMatched extends MatchResult + final case class Matched[A](extracted: Set[A]) extends MatchResult[A] + + object NotMatched extends MatchResult[Nothing] + } + + final case class ClassCanonicalName(value: String) extends AnyVal + + private[datastreams] trait ReflectionBasedDataStreamsEsContextCreator { + def actionRequestClass: ClassCanonicalName + + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] + + protected def tryMatchActionRequestWithIndices(actionRequest: ActionRequest, + getIndicesMethodName: String): MatchResult[ClusterIndexName] = { + tryMatchActionRequest[ClusterIndexName]( + actionRequest = actionRequest, + getPropsMethodName = getIndicesMethodName, + toDomain = ClusterIndexName.fromString + ) + } + + protected def tryMatchActionRequestWithDataStreams(actionRequest: ActionRequest, + getDataStreamsMethodName: String): MatchResult[DataStreamName] = { + tryMatchActionRequest[DataStreamName]( + actionRequest = actionRequest, + getPropsMethodName = getDataStreamsMethodName, + toDomain = DataStreamName.fromString + ) + } + + private def tryMatchActionRequest[A](actionRequest: ActionRequest, + getPropsMethodName: String, + toDomain: String => Option[A]): MatchResult[A] = { + Option(actionRequest.getClass.getCanonicalName) + .find(_ === actionRequestClass.value) + .map { _ => + Matched.apply[A] { + extractStringArrayFromPrivateMethod(getPropsMethodName, actionRequest) + .asSafeList + .toSet + .flatMap((value: String) => toDomain(value)) + } + } + .getOrElse(NotMatched) + } + } } \ No newline at end of file diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 4f7d9febed..623b2dfc29 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -68,6 +68,7 @@ import tech.beshu.ror.es.actions.RorActionRequest import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventRequest import tech.beshu.ror.es.actions.rrmetadata.RRUserMetadataRequest import tech.beshu.ror.es.handler.AclAwareRequestFilter._ +import tech.beshu.ror.es.handler.request.ActionRequestOps._ import tech.beshu.ror.es.handler.request.RestRequestOps._ import tech.beshu.ror.es.handler.request.context.types._ import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext @@ -202,7 +203,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new RolloverEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ResolveIndexAction.Request => regularRequestHandler.handle(new ResolveIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) - case request: IndicesRequest.Replaceable => + case request: IndicesRequest.Replaceable if request.notDataStreamRelated => regularRequestHandler.handle(new IndicesReplaceableEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ReindexRequest => regularRequestHandler.handle(new ReindexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala new file mode 100644 index 0000000000..09fbbc8857 --- /dev/null +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala @@ -0,0 +1,38 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request + +import org.elasticsearch.action.ActionRequest +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext + +object ActionRequestOps { + + implicit class RequestOps(val actionRequest: ActionRequest) extends AnyVal { + + def notDataStreamRelated: Boolean = { + !RequestOps + .dataStreamRequests + .contains(actionRequest.getClass.getCanonicalName) + } + } + + private object RequestOps { + private val dataStreamRequests: Set[String] = + ReflectionBasedDataStreamsEsRequestContext.supportedActionRequests.map(_.value) + } + +} diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index cfa253d507..6ff5ce4a0e 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala deleted file mode 100644 index 17c8c7d5b0..0000000000 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.common.util.set.Sets -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext -import tech.beshu.ror.utils.ReflecUtils - -import scala.collection.JavaConverters._ - -private[datastreams] abstract class BaseReadDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (tryUpdate(actionRequest, filteredIndices)) Modified - else { - logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") - ShouldBeInterrupted - } - } - - private def tryUpdate(actionRequest: R, indices: NonEmptyList[ClusterIndexName]) = { - // Optimistic reflection attempt - ReflecUtils.setIndices( - actionRequest, - Sets.newHashSet(setIndicesMethodName), - indices.toList.map(_.stringify).toSet.asJava - ) - } - - protected def setIndicesMethodName: String -} diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala deleted file mode 100644 index 939595ea5a..0000000000 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext - -private[datastreams] abstract class BaseWriteDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (indices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } - } -} diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala index a14811e9a8..3adf6011ad 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala @@ -18,33 +18,45 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class CreateDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = dataStreams + + override def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -private[datastreams] object CreateDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +private[datastreams] object CreateDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala index 98ad908357..537241eaad 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala @@ -16,36 +16,60 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DataStreamsStatsEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "indices", + dataStreams = blockContext.dataStreams + ) + } - override protected def setIndicesMethodName: String = "indices" } -object DataStreamsStatsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DataStreamsStatsEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala index 0a22f6be32..b12dddc61f 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala @@ -16,36 +16,59 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DeleteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override protected def setIndicesMethodName: String = "indices" + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } } -object DeleteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DeleteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala index ef6c28966e..27d887adc6 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala @@ -16,36 +16,125 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import org.elasticsearch.action.ActionRequest +import cats.implicits.toShow +import monix.eval.Task +import org.elasticsearch.action.{ActionRequest, ActionResponse} import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils.{invokeMethod, setField} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.collection.JavaConverters._ +import scala.util.Try private[datastreams] class GetDataStreamEsRequestContext(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, + allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.UpdateResponse { + case r: ActionResponse if isGetDataStreamActionResponse(r) => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(_, allAllowedIndices) => + Task.now(updateActionResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + + } + + private def modifyActionRequest(blockContext: BlockContext.DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } + + private def isGetDataStreamActionResponse(r: ActionResponse) = { + r.getClass.getCanonicalName == "org.elasticsearch.xpack.core.action.GetDataStreamAction.Response" + } - override protected def setIndicesMethodName: String = "indices" + + private def updateActionResponse(response: ActionResponse, + allAllowedIndices: Set[ClusterIndexName]): ActionResponse = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredDataStreams = + invokeMethod(response, response.getClass, "getDataStreams") + .asInstanceOf[java.util.List[Object]] + .asScala + .filter { dataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + + setField(response, response.getClass, "dataStreams", filteredDataStreams.asJava) + response + } + + private def backingIndiesMatchesAllowedIndices(info: Object, allowedIndicesMatcher: Matcher[ClusterIndexName]): Boolean = { + val dataStreamIndices = indicesFromDataStreamInfo(info).get + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty + } + + private def indicesFromDataStreamInfo(info: Object): Try[Set[ClusterIndexName]] = { + for { + dataStream <- Try(invokeMethod(info, info.getClass, "getDataStream")) + backingIndices <- Try { + invokeMethod(dataStream, dataStream.getClass, "getIndices") + .asInstanceOf[java.util.List[Object]] + .asSafeList + } + indices <- Try { + backingIndices + .flatMap(backingIndex => Option(invokeMethod(backingIndex, backingIndex.getClass, "getName").asInstanceOf[String])) + .flatMap(ClusterIndexName.fromString) + .toSet + } + } yield indices + + } } -object GetDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object GetDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.GetDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala index 8b1a5b432c..9b730f7210 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala @@ -18,33 +18,46 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} class MigrateToDataStreamEsRequestContext(actionRequest: ActionRequest, indices: Set[ClusterIndexName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = Set.empty + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved(indices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL + } } -object MigrateToDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { +object MigrateToDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { + tryMatchActionRequestWithIndices( + actionRequest = arg.esContext.actionRequest, + getIndicesMethodName = "indices" + ) match { case MatchResult.Matched(indices) => - Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala index bf1d00459a..86be1ed7c4 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala @@ -18,32 +18,43 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class PromoteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -object PromoteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object PromoteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala index deb036f7f0..39fab97548 100644 --- a/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala +++ b/es714x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala @@ -16,44 +16,100 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList import cats.implicits._ import org.elasticsearch.action.ActionRequest -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import org.elasticsearch.common.util.set.Sets +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult.{Matched, NotMatched} -import tech.beshu.ror.es.handler.request.context.types.{BaseIndicesEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils import tech.beshu.ror.utils.ReflecUtils.extractStringArrayFromPrivateMethod import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.collection.JavaConverters._ object ReflectionBasedDataStreamsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[BaseIndicesEsRequestContext[ActionRequest]] = { - CreateDataStreamEsRequestContext.unapply(arg) - .orElse(DataStreamsStatsEsRequestContext.unapply(arg)) - .orElse(DeleteDataStreamEsRequestContext.unapply(arg)) - .orElse(GetDataStreamEsRequestContext.unapply(arg)) - .orElse(MigrateToDataStreamEsRequestContext.unapply(arg)) - .orElse(PromoteDataStreamEsRequestContext.unapply(arg)) + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] = { + esContextCreators + .toStream + .flatMap(_.unapply(arg)) + .headOption } - private[datastreams] def tryMatchActionRequest(actionRequest: ActionRequest, - expectedClassCanonicalName: String, - getIndicesMethodName: String): MatchResult = { - Option(actionRequest.getClass.getCanonicalName) - .find(_ == expectedClassCanonicalName) - .flatMap { _ => - NonEmptyList - .fromList(extractStringArrayFromPrivateMethod(getIndicesMethodName, actionRequest).asSafeList) - .map(_.toList.toSet.flatMap(ClusterIndexName.fromString)) - .map(Matched.apply) - } - .getOrElse(NotMatched) + val supportedActionRequests: Set[ClassCanonicalName] = esContextCreators.map(_.actionRequestClass).toSet + + private lazy val esContextCreators: UniqueNonEmptyList[ReflectionBasedDataStreamsEsContextCreator] = UniqueNonEmptyList.of( + CreateDataStreamEsRequestContext, + DataStreamsStatsEsRequestContext, + DeleteDataStreamEsRequestContext, + GetDataStreamEsRequestContext, + MigrateToDataStreamEsRequestContext, + PromoteDataStreamEsRequestContext + ) + + private[datastreams] def tryUpdateDataStreams[R <: ActionRequest](actionRequest: R, + dataStreamsFieldName: String, + dataStreams: Set[DataStreamName]): Boolean = { + // Optimistic reflection attempt + ReflecUtils.setIndices( + actionRequest, + Sets.newHashSet(dataStreamsFieldName), + dataStreams.toList.map(DataStreamName.toString).toSet.asJava + ) } - private[datastreams] sealed trait MatchResult + + private[datastreams] sealed trait MatchResult[+A] + private[datastreams] object MatchResult { - final case class Matched(extractedIndices: Set[ClusterIndexName]) extends MatchResult - object NotMatched extends MatchResult + final case class Matched[A](extracted: Set[A]) extends MatchResult[A] + + object NotMatched extends MatchResult[Nothing] + } + + final case class ClassCanonicalName(value: String) extends AnyVal + + private[datastreams] trait ReflectionBasedDataStreamsEsContextCreator { + def actionRequestClass: ClassCanonicalName + + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] + + protected def tryMatchActionRequestWithIndices(actionRequest: ActionRequest, + getIndicesMethodName: String): MatchResult[ClusterIndexName] = { + tryMatchActionRequest[ClusterIndexName]( + actionRequest = actionRequest, + getPropsMethodName = getIndicesMethodName, + toDomain = ClusterIndexName.fromString + ) + } + + protected def tryMatchActionRequestWithDataStreams(actionRequest: ActionRequest, + getDataStreamsMethodName: String): MatchResult[DataStreamName] = { + tryMatchActionRequest[DataStreamName]( + actionRequest = actionRequest, + getPropsMethodName = getDataStreamsMethodName, + toDomain = DataStreamName.fromString + ) + } + + private def tryMatchActionRequest[A](actionRequest: ActionRequest, + getPropsMethodName: String, + toDomain: String => Option[A]): MatchResult[A] = { + Option(actionRequest.getClass.getCanonicalName) + .find(_ === actionRequestClass.value) + .map { _ => + Matched.apply[A] { + extractStringArrayFromPrivateMethod(getPropsMethodName, actionRequest) + .asSafeList + .toSet + .flatMap((value: String) => toDomain(value)) + } + } + .getOrElse(NotMatched) + } + } } \ No newline at end of file diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 1ae5e95284..ca98c436fb 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -70,6 +70,7 @@ import tech.beshu.ror.es.actions.RorActionRequest import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventRequest import tech.beshu.ror.es.actions.rrmetadata.RRUserMetadataRequest import tech.beshu.ror.es.handler.AclAwareRequestFilter._ +import tech.beshu.ror.es.handler.request.ActionRequestOps._ import tech.beshu.ror.es.handler.request.RestRequestOps._ import tech.beshu.ror.es.handler.request.context.types._ import tech.beshu.ror.es.{ResponseFieldsFiltering, RorClusterService} @@ -108,7 +109,6 @@ class AclAwareRequestFilter(clusterService: RorClusterService, private def handleEsRestApiRequest(regularRequestHandler: RegularRequestHandler, esContext: EsContext, aclContext: AccessControlStaticContext) = { - esContext.actionRequest match { case request: RRAuditEventRequest => regularRequestHandler.handle(new AuditEventESRequestContext(request, esContext, clusterService, threadPool)) @@ -166,7 +166,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) // data streams case request: ModifyDataStreamsAction.Request => - regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) // indices case request: GetIndexRequest => regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) @@ -207,7 +207,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new RolloverEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ResolveIndexAction.Request => regularRequestHandler.handle(new ResolveIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) - case request: IndicesRequest.Replaceable => + case request: IndicesRequest.Replaceable if request.notDataStreamRelated => regularRequestHandler.handle(new IndicesReplaceableEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ReindexRequest => regularRequestHandler.handle(new ReindexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala new file mode 100644 index 0000000000..09fbbc8857 --- /dev/null +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala @@ -0,0 +1,38 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request + +import org.elasticsearch.action.ActionRequest +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext + +object ActionRequestOps { + + implicit class RequestOps(val actionRequest: ActionRequest) extends AnyVal { + + def notDataStreamRelated: Boolean = { + !RequestOps + .dataStreamRequests + .contains(actionRequest.getClass.getCanonicalName) + } + } + + private object RequestOps { + private val dataStreamRequests: Set[String] = + ReflectionBasedDataStreamsEsRequestContext.supportedActionRequests.map(_.value) + } + +} diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index cfa253d507..6ff5ce4a0e 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala deleted file mode 100644 index 17ec1eea97..0000000000 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.common.util.set.Sets -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext -import tech.beshu.ror.utils.ReflecUtils - -import scala.collection.JavaConverters._ - -private[datastreams] abstract class BaseReadDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (tryUpdate(actionRequest, filteredIndices)) Modified - else { - logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") - ShouldBeInterrupted - } - } - - private def tryUpdate(actionRequest: R, indices: NonEmptyList[ClusterIndexName]) = { - // Optimistic reflection attempt - ReflecUtils.setIndices( - actionRequest, - Sets.newHashSet(setIndicesMethodName), - indices.toList.map(_.stringify).toSet.asJava - ) - } - - protected def setIndicesMethodName: String -} \ No newline at end of file diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala deleted file mode 100644 index c40a6bed7f..0000000000 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits.toShow -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.common.util.set.Sets -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext -import tech.beshu.ror.utils.ReflecUtils - -import scala.collection.JavaConverters._ - -private[datastreams] abstract class BaseWriteDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (indices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } - } -} diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala index a14811e9a8..3adf6011ad 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala @@ -18,33 +18,45 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class CreateDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = dataStreams + + override def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -private[datastreams] object CreateDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +private[datastreams] object CreateDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala index 98ad908357..537241eaad 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala @@ -16,36 +16,60 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DataStreamsStatsEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "indices", + dataStreams = blockContext.dataStreams + ) + } - override protected def setIndicesMethodName: String = "indices" } -object DataStreamsStatsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DataStreamsStatsEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala index 0a22f6be32..b12dddc61f 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala @@ -16,36 +16,59 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DeleteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override protected def setIndicesMethodName: String = "indices" + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } } -object DeleteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DeleteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala index ef6c28966e..27d887adc6 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala @@ -16,36 +16,125 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import org.elasticsearch.action.ActionRequest +import cats.implicits.toShow +import monix.eval.Task +import org.elasticsearch.action.{ActionRequest, ActionResponse} import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils.{invokeMethod, setField} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.collection.JavaConverters._ +import scala.util.Try private[datastreams] class GetDataStreamEsRequestContext(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, + allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.UpdateResponse { + case r: ActionResponse if isGetDataStreamActionResponse(r) => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(_, allAllowedIndices) => + Task.now(updateActionResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + + } + + private def modifyActionRequest(blockContext: BlockContext.DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } + + private def isGetDataStreamActionResponse(r: ActionResponse) = { + r.getClass.getCanonicalName == "org.elasticsearch.xpack.core.action.GetDataStreamAction.Response" + } - override protected def setIndicesMethodName: String = "indices" + + private def updateActionResponse(response: ActionResponse, + allAllowedIndices: Set[ClusterIndexName]): ActionResponse = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredDataStreams = + invokeMethod(response, response.getClass, "getDataStreams") + .asInstanceOf[java.util.List[Object]] + .asScala + .filter { dataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + + setField(response, response.getClass, "dataStreams", filteredDataStreams.asJava) + response + } + + private def backingIndiesMatchesAllowedIndices(info: Object, allowedIndicesMatcher: Matcher[ClusterIndexName]): Boolean = { + val dataStreamIndices = indicesFromDataStreamInfo(info).get + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty + } + + private def indicesFromDataStreamInfo(info: Object): Try[Set[ClusterIndexName]] = { + for { + dataStream <- Try(invokeMethod(info, info.getClass, "getDataStream")) + backingIndices <- Try { + invokeMethod(dataStream, dataStream.getClass, "getIndices") + .asInstanceOf[java.util.List[Object]] + .asSafeList + } + indices <- Try { + backingIndices + .flatMap(backingIndex => Option(invokeMethod(backingIndex, backingIndex.getClass, "getName").asInstanceOf[String])) + .flatMap(ClusterIndexName.fromString) + .toSet + } + } yield indices + + } } -object GetDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object GetDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.GetDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala index 8b1a5b432c..9b730f7210 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala @@ -18,33 +18,46 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} class MigrateToDataStreamEsRequestContext(actionRequest: ActionRequest, indices: Set[ClusterIndexName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = Set.empty + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved(indices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL + } } -object MigrateToDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { +object MigrateToDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { + tryMatchActionRequestWithIndices( + actionRequest = arg.esContext.actionRequest, + getIndicesMethodName = "indices" + ) match { case MatchResult.Matched(indices) => - Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala index e67e01c647..04459fa030 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala @@ -16,39 +16,36 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.ModifyDataStreamsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext +import tech.beshu.ror.es.handler.request.context.types.BaseDataStreamsEsRequestContext import tech.beshu.ror.utils.ScalaOps._ class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet } - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted + override protected def backingIndicesFrom(request: ModifyDataStreamsAction.Request): DataStreamRequestBlockContext.BackingIndices = + BackingIndices.IndicesInvolved(originIndices, Set(ClusterIndexName.Local.wildcard)) + + override def dataStreamsFrom(request: ModifyDataStreamsAction.Request): Set[domain.DataStreamName] = { + request.getActions.asSafeList.map(_.getDataStream).flatMap(DataStreamName.fromString).toSet } - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala index bf1d00459a..86be1ed7c4 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala @@ -18,32 +18,43 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class PromoteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -object PromoteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object PromoteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala index deb036f7f0..39fab97548 100644 --- a/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala +++ b/es716x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala @@ -16,44 +16,100 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList import cats.implicits._ import org.elasticsearch.action.ActionRequest -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import org.elasticsearch.common.util.set.Sets +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult.{Matched, NotMatched} -import tech.beshu.ror.es.handler.request.context.types.{BaseIndicesEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils import tech.beshu.ror.utils.ReflecUtils.extractStringArrayFromPrivateMethod import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.collection.JavaConverters._ object ReflectionBasedDataStreamsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[BaseIndicesEsRequestContext[ActionRequest]] = { - CreateDataStreamEsRequestContext.unapply(arg) - .orElse(DataStreamsStatsEsRequestContext.unapply(arg)) - .orElse(DeleteDataStreamEsRequestContext.unapply(arg)) - .orElse(GetDataStreamEsRequestContext.unapply(arg)) - .orElse(MigrateToDataStreamEsRequestContext.unapply(arg)) - .orElse(PromoteDataStreamEsRequestContext.unapply(arg)) + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] = { + esContextCreators + .toStream + .flatMap(_.unapply(arg)) + .headOption } - private[datastreams] def tryMatchActionRequest(actionRequest: ActionRequest, - expectedClassCanonicalName: String, - getIndicesMethodName: String): MatchResult = { - Option(actionRequest.getClass.getCanonicalName) - .find(_ == expectedClassCanonicalName) - .flatMap { _ => - NonEmptyList - .fromList(extractStringArrayFromPrivateMethod(getIndicesMethodName, actionRequest).asSafeList) - .map(_.toList.toSet.flatMap(ClusterIndexName.fromString)) - .map(Matched.apply) - } - .getOrElse(NotMatched) + val supportedActionRequests: Set[ClassCanonicalName] = esContextCreators.map(_.actionRequestClass).toSet + + private lazy val esContextCreators: UniqueNonEmptyList[ReflectionBasedDataStreamsEsContextCreator] = UniqueNonEmptyList.of( + CreateDataStreamEsRequestContext, + DataStreamsStatsEsRequestContext, + DeleteDataStreamEsRequestContext, + GetDataStreamEsRequestContext, + MigrateToDataStreamEsRequestContext, + PromoteDataStreamEsRequestContext + ) + + private[datastreams] def tryUpdateDataStreams[R <: ActionRequest](actionRequest: R, + dataStreamsFieldName: String, + dataStreams: Set[DataStreamName]): Boolean = { + // Optimistic reflection attempt + ReflecUtils.setIndices( + actionRequest, + Sets.newHashSet(dataStreamsFieldName), + dataStreams.toList.map(DataStreamName.toString).toSet.asJava + ) } - private[datastreams] sealed trait MatchResult + + private[datastreams] sealed trait MatchResult[+A] + private[datastreams] object MatchResult { - final case class Matched(extractedIndices: Set[ClusterIndexName]) extends MatchResult - object NotMatched extends MatchResult + final case class Matched[A](extracted: Set[A]) extends MatchResult[A] + + object NotMatched extends MatchResult[Nothing] + } + + final case class ClassCanonicalName(value: String) extends AnyVal + + private[datastreams] trait ReflectionBasedDataStreamsEsContextCreator { + def actionRequestClass: ClassCanonicalName + + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] + + protected def tryMatchActionRequestWithIndices(actionRequest: ActionRequest, + getIndicesMethodName: String): MatchResult[ClusterIndexName] = { + tryMatchActionRequest[ClusterIndexName]( + actionRequest = actionRequest, + getPropsMethodName = getIndicesMethodName, + toDomain = ClusterIndexName.fromString + ) + } + + protected def tryMatchActionRequestWithDataStreams(actionRequest: ActionRequest, + getDataStreamsMethodName: String): MatchResult[DataStreamName] = { + tryMatchActionRequest[DataStreamName]( + actionRequest = actionRequest, + getPropsMethodName = getDataStreamsMethodName, + toDomain = DataStreamName.fromString + ) + } + + private def tryMatchActionRequest[A](actionRequest: ActionRequest, + getPropsMethodName: String, + toDomain: String => Option[A]): MatchResult[A] = { + Option(actionRequest.getClass.getCanonicalName) + .find(_ === actionRequestClass.value) + .map { _ => + Matched.apply[A] { + extractStringArrayFromPrivateMethod(getPropsMethodName, actionRequest) + .asSafeList + .toSet + .flatMap((value: String) => toDomain(value)) + } + } + .getOrElse(NotMatched) + } + } } \ No newline at end of file diff --git a/es72x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es72x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es72x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es72x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es73x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es73x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es73x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es73x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es74x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es74x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es74x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es74x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es77x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es77x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es77x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es77x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es78x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es78x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es78x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es78x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 4f7d9febed..623b2dfc29 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -68,6 +68,7 @@ import tech.beshu.ror.es.actions.RorActionRequest import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventRequest import tech.beshu.ror.es.actions.rrmetadata.RRUserMetadataRequest import tech.beshu.ror.es.handler.AclAwareRequestFilter._ +import tech.beshu.ror.es.handler.request.ActionRequestOps._ import tech.beshu.ror.es.handler.request.RestRequestOps._ import tech.beshu.ror.es.handler.request.context.types._ import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext @@ -202,7 +203,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new RolloverEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ResolveIndexAction.Request => regularRequestHandler.handle(new ResolveIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) - case request: IndicesRequest.Replaceable => + case request: IndicesRequest.Replaceable if request.notDataStreamRelated => regularRequestHandler.handle(new IndicesReplaceableEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ReindexRequest => regularRequestHandler.handle(new ReindexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala new file mode 100644 index 0000000000..09fbbc8857 --- /dev/null +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala @@ -0,0 +1,38 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request + +import org.elasticsearch.action.ActionRequest +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext + +object ActionRequestOps { + + implicit class RequestOps(val actionRequest: ActionRequest) extends AnyVal { + + def notDataStreamRelated: Boolean = { + !RequestOps + .dataStreamRequests + .contains(actionRequest.getClass.getCanonicalName) + } + } + + private object RequestOps { + private val dataStreamRequests: Set[String] = + ReflectionBasedDataStreamsEsRequestContext.supportedActionRequests.map(_.value) + } + +} diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index cfa253d507..6ff5ce4a0e 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala deleted file mode 100644 index 17c8c7d5b0..0000000000 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.common.util.set.Sets -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext -import tech.beshu.ror.utils.ReflecUtils - -import scala.collection.JavaConverters._ - -private[datastreams] abstract class BaseReadDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (tryUpdate(actionRequest, filteredIndices)) Modified - else { - logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") - ShouldBeInterrupted - } - } - - private def tryUpdate(actionRequest: R, indices: NonEmptyList[ClusterIndexName]) = { - // Optimistic reflection attempt - ReflecUtils.setIndices( - actionRequest, - Sets.newHashSet(setIndicesMethodName), - indices.toList.map(_.stringify).toSet.asJava - ) - } - - protected def setIndicesMethodName: String -} diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala deleted file mode 100644 index 939595ea5a..0000000000 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext - -private[datastreams] abstract class BaseWriteDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (indices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } - } -} diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala index a14811e9a8..3adf6011ad 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala @@ -18,33 +18,45 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class CreateDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = dataStreams + + override def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -private[datastreams] object CreateDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +private[datastreams] object CreateDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala index 98ad908357..537241eaad 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala @@ -16,36 +16,60 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DataStreamsStatsEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "indices", + dataStreams = blockContext.dataStreams + ) + } - override protected def setIndicesMethodName: String = "indices" } -object DataStreamsStatsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DataStreamsStatsEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala index 0a22f6be32..b12dddc61f 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala @@ -16,36 +16,59 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DeleteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override protected def setIndicesMethodName: String = "indices" + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } } -object DeleteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DeleteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala index ef6c28966e..27d887adc6 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala @@ -16,36 +16,125 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import org.elasticsearch.action.ActionRequest +import cats.implicits.toShow +import monix.eval.Task +import org.elasticsearch.action.{ActionRequest, ActionResponse} import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils.{invokeMethod, setField} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.collection.JavaConverters._ +import scala.util.Try private[datastreams] class GetDataStreamEsRequestContext(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, + allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.UpdateResponse { + case r: ActionResponse if isGetDataStreamActionResponse(r) => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(_, allAllowedIndices) => + Task.now(updateActionResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + + } + + private def modifyActionRequest(blockContext: BlockContext.DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } + + private def isGetDataStreamActionResponse(r: ActionResponse) = { + r.getClass.getCanonicalName == "org.elasticsearch.xpack.core.action.GetDataStreamAction.Response" + } - override protected def setIndicesMethodName: String = "indices" + + private def updateActionResponse(response: ActionResponse, + allAllowedIndices: Set[ClusterIndexName]): ActionResponse = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredDataStreams = + invokeMethod(response, response.getClass, "getDataStreams") + .asInstanceOf[java.util.List[Object]] + .asScala + .filter { dataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + + setField(response, response.getClass, "dataStreams", filteredDataStreams.asJava) + response + } + + private def backingIndiesMatchesAllowedIndices(info: Object, allowedIndicesMatcher: Matcher[ClusterIndexName]): Boolean = { + val dataStreamIndices = indicesFromDataStreamInfo(info).get + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty + } + + private def indicesFromDataStreamInfo(info: Object): Try[Set[ClusterIndexName]] = { + for { + dataStream <- Try(invokeMethod(info, info.getClass, "getDataStream")) + backingIndices <- Try { + invokeMethod(dataStream, dataStream.getClass, "getIndices") + .asInstanceOf[java.util.List[Object]] + .asSafeList + } + indices <- Try { + backingIndices + .flatMap(backingIndex => Option(invokeMethod(backingIndex, backingIndex.getClass, "getName").asInstanceOf[String])) + .flatMap(ClusterIndexName.fromString) + .toSet + } + } yield indices + + } } -object GetDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object GetDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.GetDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala index 86c8e30a13..5b080621cd 100644 --- a/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala +++ b/es79x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala @@ -16,42 +16,89 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList import cats.implicits._ import org.elasticsearch.action.ActionRequest -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import org.elasticsearch.common.util.set.Sets +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult.{Matched, NotMatched} -import tech.beshu.ror.es.handler.request.context.types.{BaseIndicesEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils import tech.beshu.ror.utils.ReflecUtils.extractStringArrayFromPrivateMethod import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.collection.JavaConverters._ object ReflectionBasedDataStreamsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[BaseIndicesEsRequestContext[ActionRequest]] = { - CreateDataStreamEsRequestContext.unapply(arg) - .orElse(DataStreamsStatsEsRequestContext.unapply(arg)) - .orElse(DeleteDataStreamEsRequestContext.unapply(arg)) - .orElse(GetDataStreamEsRequestContext.unapply(arg)) + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] = { + esContextCreators + .toStream + .flatMap(_.unapply(arg)) + .headOption } - private[datastreams] def tryMatchActionRequest(actionRequest: ActionRequest, - expectedClassCanonicalName: String, - getIndicesMethodName: String): MatchResult = { - Option(actionRequest.getClass.getCanonicalName) - .find(_ == expectedClassCanonicalName) - .flatMap { _ => - NonEmptyList - .fromList(extractStringArrayFromPrivateMethod(getIndicesMethodName, actionRequest).asSafeList) - .map(_.toList.toSet.flatMap(ClusterIndexName.fromString)) - .map(Matched.apply) - } - .getOrElse(NotMatched) + val supportedActionRequests: Set[ClassCanonicalName] = esContextCreators.map(_.actionRequestClass).toSet + + private lazy val esContextCreators: UniqueNonEmptyList[ReflectionBasedDataStreamsEsContextCreator] = UniqueNonEmptyList.of( + CreateDataStreamEsRequestContext, + DataStreamsStatsEsRequestContext, + DeleteDataStreamEsRequestContext, + GetDataStreamEsRequestContext, + ) + + private[datastreams] def tryUpdateDataStreams[R <: ActionRequest](actionRequest: R, + dataStreamsFieldName: String, + dataStreams: Set[DataStreamName]): Boolean = { + // Optimistic reflection attempt + ReflecUtils.setIndices( + actionRequest, + Sets.newHashSet(dataStreamsFieldName), + dataStreams.toList.map(DataStreamName.toString).toSet.asJava + ) } - private[datastreams] sealed trait MatchResult + + private[datastreams] sealed trait MatchResult[+A] + private[datastreams] object MatchResult { - final case class Matched(extractedIndices: Set[ClusterIndexName]) extends MatchResult - object NotMatched extends MatchResult + final case class Matched[A](extracted: Set[A]) extends MatchResult[A] + + object NotMatched extends MatchResult[Nothing] + } + + final case class ClassCanonicalName(value: String) extends AnyVal + + private[datastreams] trait ReflectionBasedDataStreamsEsContextCreator { + def actionRequestClass: ClassCanonicalName + + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] + + protected def tryMatchActionRequestWithDataStreams(actionRequest: ActionRequest, + getDataStreamsMethodName: String): MatchResult[DataStreamName] = { + tryMatchActionRequest[DataStreamName]( + actionRequest = actionRequest, + getPropsMethodName = getDataStreamsMethodName, + toDomain = DataStreamName.fromString + ) + } + + private def tryMatchActionRequest[A](actionRequest: ActionRequest, + getPropsMethodName: String, + toDomain: String => Option[A]): MatchResult[A] = { + Option(actionRequest.getClass.getCanonicalName) + .find(_ === actionRequestClass.value) + .map { _ => + Matched.apply[A] { + extractStringArrayFromPrivateMethod(getPropsMethodName, actionRequest) + .asSafeList + .toSet + .flatMap((value: String) => toDomain(value)) + } + } + .getOrElse(NotMatched) + } + } } \ No newline at end of file diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 01e2bd1c49..72e515fef8 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -48,8 +48,8 @@ import org.elasticsearch.action.admin.indices.template.get.{GetComponentTemplate import org.elasticsearch.action.admin.indices.template.post.{SimulateIndexTemplateRequest, SimulateTemplateAction} import org.elasticsearch.action.admin.indices.template.put.{PutComponentTemplateAction, PutComposableIndexTemplateAction, PutIndexTemplateRequest} import org.elasticsearch.action.bulk.{BulkRequest, BulkShardRequest} -import org.elasticsearch.action.delete.DeleteRequest import org.elasticsearch.action.datastreams.ModifyDataStreamsAction +import org.elasticsearch.action.delete.DeleteRequest import org.elasticsearch.action.get.{GetRequest, MultiGetRequest} import org.elasticsearch.action.index.IndexRequest import org.elasticsearch.action.search.{MultiSearchRequest, SearchRequest} @@ -60,7 +60,6 @@ import org.elasticsearch.index.reindex.ReindexRequest import org.elasticsearch.rest.RestChannel import org.elasticsearch.tasks.{Task => EsTask} import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.es.handler.request.context.types.datastreams.{ModifyDataStreamsEsRequestContext, ReflectionBasedDataStreamsEsRequestContext} import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext import tech.beshu.ror.accesscontrol.domain.{Action, Header} import tech.beshu.ror.accesscontrol.matchers.UniqueIdentifierGenerator @@ -70,8 +69,10 @@ import tech.beshu.ror.es.actions.RorActionRequest import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventRequest import tech.beshu.ror.es.actions.rrmetadata.RRUserMetadataRequest import tech.beshu.ror.es.handler.AclAwareRequestFilter._ +import tech.beshu.ror.es.handler.request.ActionRequestOps._ import tech.beshu.ror.es.handler.request.RestRequestOps._ import tech.beshu.ror.es.handler.request.context.types._ +import tech.beshu.ror.es.handler.request.context.types.datastreams.{ModifyDataStreamsEsRequestContext, ReflectionBasedDataStreamsEsRequestContext} import tech.beshu.ror.es.{ResponseFieldsFiltering, RorClusterService} import java.time.Instant @@ -165,7 +166,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) // data streams case request: ModifyDataStreamsAction.Request => - regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) // indices case request: GetIndexRequest => regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) @@ -206,7 +207,7 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new RolloverEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ResolveIndexAction.Request => regularRequestHandler.handle(new ResolveIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) - case request: IndicesRequest.Replaceable => + case request: IndicesRequest.Replaceable if request.notDataStreamRelated => regularRequestHandler.handle(new IndicesReplaceableEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) case request: ReindexRequest => regularRequestHandler.handle(new ReindexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala new file mode 100644 index 0000000000..09fbbc8857 --- /dev/null +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/ActionRequestOps.scala @@ -0,0 +1,38 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request + +import org.elasticsearch.action.ActionRequest +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext + +object ActionRequestOps { + + implicit class RequestOps(val actionRequest: ActionRequest) extends AnyVal { + + def notDataStreamRelated: Boolean = { + !RequestOps + .dataStreamRequests + .contains(actionRequest.getClass.getCanonicalName) + } + } + + private object RequestOps { + private val dataStreamRequests: Set[String] = + ReflectionBasedDataStreamsEsRequestContext.supportedActionRequests.map(_.value) + } + +} diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index fdf50a2563..b8abe1ff7a 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala deleted file mode 100644 index a91662a648..0000000000 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.datastreams.ModifyDataStreamsAction -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ - -class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet - } - - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } - - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) - } -} diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala deleted file mode 100644 index 17c8c7d5b0..0000000000 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseReadDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.common.util.set.Sets -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext -import tech.beshu.ror.utils.ReflecUtils - -import scala.collection.JavaConverters._ - -private[datastreams] abstract class BaseReadDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (tryUpdate(actionRequest, filteredIndices)) Modified - else { - logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") - ShouldBeInterrupted - } - } - - private def tryUpdate(actionRequest: R, indices: NonEmptyList[ClusterIndexName]) = { - // Optimistic reflection attempt - ReflecUtils.setIndices( - actionRequest, - Sets.newHashSet(setIndicesMethodName), - indices.toList.map(_.stringify).toSet.asJava - ) - } - - protected def setIndicesMethodName: String -} diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala deleted file mode 100644 index 939595ea5a..0000000000 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/BaseWriteDataStreamsEsRequestContext.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.es.handler.request.context.types.datastreams - -import cats.data.NonEmptyList -import cats.implicits._ -import org.elasticsearch.action.ActionRequest -import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName -import tech.beshu.ror.es.RorClusterService -import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext - -private[datastreams] abstract class BaseWriteDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, - indices: Set[ClusterIndexName], - esContext: EsContext, - aclContext: AccessControlStaticContext, - clusterService: RorClusterService, - override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { - - override def indicesFrom(request: R): Set[ClusterIndexName] = indices - - override def update(request: R, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (indices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } - } -} diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala index a14811e9a8..3adf6011ad 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala @@ -18,33 +18,45 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class CreateDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = dataStreams + + override def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -private[datastreams] object CreateDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +private[datastreams] object CreateDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.CreateDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[CreateDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new CreateDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala index 98ad908357..537241eaad 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala @@ -16,36 +16,60 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DataStreamsStatsEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "indices", + dataStreams = blockContext.dataStreams + ) + } - override protected def setIndicesMethodName: String = "indices" } -object DataStreamsStatsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DataStreamsStatsEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DataStreamsStatsAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DataStreamsStatsEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DataStreamsStatsEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala index 0a22f6be32..b12dddc61f 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala @@ -16,36 +16,59 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams +import cats.implicits.toShow import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class DeleteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override protected def setIndicesMethodName: String = "indices" + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.Modified + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + } + + private def modifyActionRequest(blockContext: DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } } -object DeleteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object DeleteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[DeleteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new DeleteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala index ef6c28966e..27d887adc6 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala @@ -16,36 +16,125 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import org.elasticsearch.action.ActionRequest +import cats.implicits.toShow +import monix.eval.Task +import org.elasticsearch.action.{ActionRequest, ActionResponse} import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator, tryUpdateDataStreams} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils.{invokeMethod, setField} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.collection.JavaConverters._ +import scala.util.Try private[datastreams] class GetDataStreamEsRequestContext(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseReadDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, + allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + if (modifyActionRequest(blockContext)) { + ModificationResult.UpdateResponse { + case r: ActionResponse if isGetDataStreamActionResponse(r) => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(_, allAllowedIndices) => + Task.now(updateActionResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getCanonicalName} request. We're using reflection to modify the request data streams and it fails. Please, report the issue.") + ModificationResult.ShouldBeInterrupted + } + + } + + private def modifyActionRequest(blockContext: BlockContext.DataStreamRequestBlockContext): Boolean = { + tryUpdateDataStreams( + actionRequest = actionRequest, + dataStreamsFieldName = "names", + dataStreams = blockContext.dataStreams + ) + } + + private def isGetDataStreamActionResponse(r: ActionResponse) = { + r.getClass.getCanonicalName == "org.elasticsearch.xpack.core.action.GetDataStreamAction.Response" + } - override protected def setIndicesMethodName: String = "indices" + + private def updateActionResponse(response: ActionResponse, + allAllowedIndices: Set[ClusterIndexName]): ActionResponse = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredDataStreams = + invokeMethod(response, response.getClass, "getDataStreams") + .asInstanceOf[java.util.List[Object]] + .asScala + .filter { dataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + + setField(response, response.getClass, "dataStreams", filteredDataStreams.asJava) + response + } + + private def backingIndiesMatchesAllowedIndices(info: Object, allowedIndicesMatcher: Matcher[ClusterIndexName]): Boolean = { + val dataStreamIndices = indicesFromDataStreamInfo(info).get + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty + } + + private def indicesFromDataStreamInfo(info: Object): Try[Set[ClusterIndexName]] = { + for { + dataStream <- Try(invokeMethod(info, info.getClass, "getDataStream")) + backingIndices <- Try { + invokeMethod(dataStream, dataStream.getClass, "getIndices") + .asInstanceOf[java.util.List[Object]] + .asSafeList + } + indices <- Try { + backingIndices + .flatMap(backingIndex => Option(invokeMethod(backingIndex, backingIndex.getClass, "getName").asInstanceOf[String])) + .flatMap(ClusterIndexName.fromString) + .toSet + } + } yield indices + + } } -object GetDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.DeleteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object GetDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.GetDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[GetDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "getNames" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new GetDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala index 8b1a5b432c..9b730f7210 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala @@ -18,33 +18,46 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} class MigrateToDataStreamEsRequestContext(actionRequest: ActionRequest, indices: Set[ClusterIndexName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[domain.DataStreamName] = Set.empty + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = + BackingIndices.IndicesInvolved(indices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL + } } -object MigrateToDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { +object MigrateToDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.MigrateToDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[MigrateToDataStreamEsRequestContext] = { + tryMatchActionRequestWithIndices( + actionRequest = arg.esContext.actionRequest, + getIndicesMethodName = "indices" + ) match { case MatchResult.Matched(indices) => - Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + Some(new MigrateToDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala index e67e01c647..04459fa030 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala @@ -16,39 +16,36 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.ModifyDataStreamsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.es.handler.request.context.types.BaseIndicesEsRequestContext +import tech.beshu.ror.es.handler.request.context.types.BaseDataStreamsEsRequestContext import tech.beshu.ror.utils.ScalaOps._ class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet } - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted + override protected def backingIndicesFrom(request: ModifyDataStreamsAction.Request): DataStreamRequestBlockContext.BackingIndices = + BackingIndices.IndicesInvolved(originIndices, Set(ClusterIndexName.Local.wildcard)) + + override def dataStreamsFrom(request: ModifyDataStreamsAction.Request): Set[domain.DataStreamName] = { + request.getActions.asSafeList.map(_.getDataStream).flatMap(DataStreamName.fromString).toSet } - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala index bf1d00459a..86be1ed7c4 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala @@ -18,32 +18,43 @@ package tech.beshu.ror.es.handler.request.context.types.datastreams import org.elasticsearch.action.ActionRequest import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext -import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest -import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.{ClassCanonicalName, MatchResult, ReflectionBasedDataStreamsEsContextCreator} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} private[datastreams] class PromoteDataStreamEsRequestContext private(actionRequest: ActionRequest, - indices: Set[ClusterIndexName], + dataStreams: Set[DataStreamName], esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseWriteDataStreamsEsRequestContext(actionRequest, indices, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def dataStreamsFrom(request: ActionRequest): Set[DataStreamName] = dataStreams + + override protected def backingIndicesFrom(request: ActionRequest): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL + } } -object PromoteDataStreamEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { - ReflectionBasedDataStreamsEsRequestContext - .tryMatchActionRequest( - actionRequest = arg.esContext.actionRequest, - expectedClassCanonicalName = "org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request", - getIndicesMethodName = "indices" - ) match { - case MatchResult.Matched(indices) => - Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, indices, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) +object PromoteDataStreamEsRequestContext extends ReflectionBasedDataStreamsEsContextCreator { + + override val actionRequestClass: ClassCanonicalName = + ClassCanonicalName("org.elasticsearch.xpack.core.action.PromoteDataStreamAction.Request") + + override def unapply(arg: ReflectionBasedActionRequest): Option[PromoteDataStreamEsRequestContext] = { + tryMatchActionRequestWithDataStreams( + actionRequest = arg.esContext.actionRequest, + getDataStreamsMethodName = "indices" + ) match { + case MatchResult.Matched(dataStreams) => + Some(new PromoteDataStreamEsRequestContext(arg.esContext.actionRequest, dataStreams, arg.esContext, arg.clusterService, arg.threadPool)) case MatchResult.NotMatched => None } diff --git a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala index deb036f7f0..aa46717854 100644 --- a/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala +++ b/es80x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ReflectionBasedDataStreamsEsRequestContext.scala @@ -16,44 +16,101 @@ */ package tech.beshu.ror.es.handler.request.context.types.datastreams -import cats.data.NonEmptyList import cats.implicits._ import org.elasticsearch.action.ActionRequest -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import org.elasticsearch.common.util.set.Sets +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.handler.request.context.types.datastreams.ReflectionBasedDataStreamsEsRequestContext.MatchResult.{Matched, NotMatched} -import tech.beshu.ror.es.handler.request.context.types.{BaseIndicesEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.es.handler.request.context.types.{BaseDataStreamsEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.utils.ReflecUtils import tech.beshu.ror.utils.ReflecUtils.extractStringArrayFromPrivateMethod import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.collection.JavaConverters._ object ReflectionBasedDataStreamsEsRequestContext { - def unapply(arg: ReflectionBasedActionRequest): Option[BaseIndicesEsRequestContext[ActionRequest]] = { - CreateDataStreamEsRequestContext.unapply(arg) - .orElse(DataStreamsStatsEsRequestContext.unapply(arg)) - .orElse(DeleteDataStreamEsRequestContext.unapply(arg)) - .orElse(GetDataStreamEsRequestContext.unapply(arg)) - .orElse(MigrateToDataStreamEsRequestContext.unapply(arg)) - .orElse(PromoteDataStreamEsRequestContext.unapply(arg)) + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] = { + esContextCreators + .toStream + .flatMap(_.unapply(arg)) + .headOption } - private[datastreams] def tryMatchActionRequest(actionRequest: ActionRequest, - expectedClassCanonicalName: String, - getIndicesMethodName: String): MatchResult = { - Option(actionRequest.getClass.getCanonicalName) - .find(_ == expectedClassCanonicalName) - .flatMap { _ => - NonEmptyList - .fromList(extractStringArrayFromPrivateMethod(getIndicesMethodName, actionRequest).asSafeList) - .map(_.toList.toSet.flatMap(ClusterIndexName.fromString)) - .map(Matched.apply) - } - .getOrElse(NotMatched) + val supportedActionRequests: Set[ClassCanonicalName] = esContextCreators.map(_.actionRequestClass).toSet + + private lazy val esContextCreators: UniqueNonEmptyList[ReflectionBasedDataStreamsEsContextCreator] = UniqueNonEmptyList.of( + CreateDataStreamEsRequestContext, + DataStreamsStatsEsRequestContext, + DeleteDataStreamEsRequestContext, + GetDataStreamEsRequestContext, + MigrateToDataStreamEsRequestContext, + PromoteDataStreamEsRequestContext + ) + + private[datastreams] def tryUpdateDataStreams[R <: ActionRequest](actionRequest: R, + dataStreamsFieldName: String, + dataStreams: Set[DataStreamName]): Boolean = { + // Optimistic reflection attempt + ReflecUtils.setIndices( + actionRequest, + Sets.newHashSet(dataStreamsFieldName), + dataStreams.toList.map(DataStreamName.toString).toSet.asJava + ) } - private[datastreams] sealed trait MatchResult + + private[datastreams] sealed trait MatchResult[+A] + private[datastreams] object MatchResult { - final case class Matched(extractedIndices: Set[ClusterIndexName]) extends MatchResult - object NotMatched extends MatchResult + final case class Matched[A](extracted: Set[A]) extends MatchResult[A] + + object NotMatched extends MatchResult[Nothing] + } + + final case class ClassCanonicalName(value: String) extends AnyVal + + private[datastreams] trait ReflectionBasedDataStreamsEsContextCreator { + + def actionRequestClass: ClassCanonicalName + + def unapply(arg: ReflectionBasedActionRequest): Option[BaseDataStreamsEsRequestContext[ActionRequest]] + + protected def tryMatchActionRequestWithIndices(actionRequest: ActionRequest, + getIndicesMethodName: String): MatchResult[ClusterIndexName] = { + tryMatchActionRequest[ClusterIndexName]( + actionRequest = actionRequest, + getPropsMethodName = getIndicesMethodName, + toDomain = ClusterIndexName.fromString + ) + } + + protected def tryMatchActionRequestWithDataStreams(actionRequest: ActionRequest, + getDataStreamsMethodName: String): MatchResult[DataStreamName] = { + tryMatchActionRequest[DataStreamName]( + actionRequest = actionRequest, + getPropsMethodName = getDataStreamsMethodName, + toDomain = DataStreamName.fromString + ) + } + + private def tryMatchActionRequest[A](actionRequest: ActionRequest, + getPropsMethodName: String, + toDomain: String => Option[A]): MatchResult[A] = { + Option(actionRequest.getClass.getCanonicalName) + .find(_ === actionRequestClass.value) + .map { _ => + Matched.apply[A] { + extractStringArrayFromPrivateMethod(getPropsMethodName, actionRequest) + .asSafeList + .toSet + .flatMap((value: String) => toDomain(value)) + } + } + .getOrElse(NotMatched) + } + } } \ No newline at end of file diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 2e6a465c6b..cb0278c6c5 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -164,19 +164,19 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) // data streams case request: CreateDataStreamAction.Request => - regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: DataStreamsStatsAction.Request => - regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, clusterService, threadPool)) case request: DeleteDataStreamAction.Request => - regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: GetDataStreamAction.Request => - regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: MigrateToDataStreamAction.Request => - regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: ModifyDataStreamsAction.Request => - regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) case request: PromoteDataStreamAction.Request => - regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) // indices case request: GetIndexRequest => regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index fdf50a2563..b8abe1ff7a 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..49b6ec1733 --- /dev/null +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + } + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala index 4eb15cc219..65fd983790 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala @@ -16,38 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.CreateDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class CreateDataStreamEsRequestContext(actionRequest: CreateDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: CreateDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: CreateDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams - override def update(request: CreateDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: CreateDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala index c6a7dbdef1..6cf03d9301 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala @@ -16,12 +16,12 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DataStreamsStatsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +29,17 @@ import tech.beshu.ror.utils.ScalaOps._ class DataStreamsStatsEsRequestContext(actionRequest: DataStreamsStatsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DataStreamsStatsAction.Request): Set[domain.ClusterIndexName] = { - getIndexFrom(request).toSet - } + override def backingIndicesFrom(request: DataStreamsStatsAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DataStreamsStatsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified - } + override def dataStreamsFrom(request: DataStreamsStatsAction.Request): Set[domain.DataStreamName] = + actionRequest.indices().asSafeList.flatMap(DataStreamName.fromString).toSet - private def getIndexFrom(request: DataStreamsStatsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + actionRequest.indices(blockContext.dataStreams.map(DataStreamName.toString).toList: _*) + ModificationResult.Modified } } \ No newline at end of file diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala index adb3bd0d82..09f25f3b35 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala @@ -16,12 +16,11 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DeleteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +28,22 @@ import tech.beshu.ror.utils.ScalaOps._ class DeleteDataStreamEsRequestContext(actionRequest: DeleteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DeleteDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet - } + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: DeleteDataStreamAction.Request): Set[DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: DeleteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DeleteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) ModificationResult.Modified } - private def getIndicesFrom(request: DeleteDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def setDataStreamNames(dataStreams: Set[DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams } } \ No newline at end of file diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala index 9e5a94f234..bed3330b45 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala @@ -16,36 +16,84 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList +import monix.eval.Task import org.elasticsearch.action.datastreams.GetDataStreamAction +import org.elasticsearch.action.datastreams.GetDataStreamAction.Response +import org.elasticsearch.index.Index import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult import tech.beshu.ror.utils.ScalaOps._ +import scala.collection.JavaConverters._ + class GetDataStreamEsRequestContext(actionRequest: GetDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: GetDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: GetDataStreamAction.Request): BackingIndices = BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) + ModificationResult.UpdateResponse { + case r: GetDataStreamAction.Response => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + Task.now(updateGetDataStreamResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } + + private def setDataStreamNames(dataStreams: Set[domain.DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams + } - override def indicesFrom(request: GetDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private def updateGetDataStreamResponse(response: GetDataStreamAction.Response, + allAllowedIndices: Set[ClusterIndexName]): GetDataStreamAction.Response = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredStreams = + response + .getDataStreams.asSafeList + .filter { dataStreamInfo: Response.DataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + new GetDataStreamAction.Response(filteredStreams.asJava) } - override def update(request: GetDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified + private def backingIndiesMatchesAllowedIndices(info: Response.DataStreamInfo, allowedIndicesMatcher: Matcher[ClusterIndexName]) = { + val dataStreamIndices: Set[ClusterIndexName] = indicesFrom(info).keySet + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty } - private def getIndicesFrom(request: GetDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def indicesFrom(response: Response.DataStreamInfo): Map[ClusterIndexName, Index] = { + response + .getDataStream + .getIndices + .asSafeList + .flatMap { index => + Option(index.getName) + .flatMap(ClusterIndexName.fromString) + .map(clusterIndexName => (clusterIndexName, index)) + } + .toMap } } diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala index 796cc63030..8d458c8f42 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala @@ -16,37 +16,31 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.MigrateToDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class MigrateToDataStreamEsRequestContext(actionRequest: MigrateToDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originIndices = Option(actionRequest.getAliasName).flatMap(ClusterIndexName.fromString).toSet - override def indicesFrom(request: MigrateToDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: MigrateToDataStreamAction.Request): Set[domain.DataStreamName] = Set.empty - override def update(request: MigrateToDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with indices requires the same set of indices after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: MigrateToDataStreamAction.Request): BackingIndices = { + BackingIndices.IndicesInvolved(filteredIndices = originIndices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + } + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala index a91662a648..bb17ff137c 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala @@ -16,13 +16,13 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.ModifyDataStreamsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -30,24 +30,22 @@ import tech.beshu.ror.utils.ScalaOps._ class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet } - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted + override protected def backingIndicesFrom(request: ModifyDataStreamsAction.Request): DataStreamRequestBlockContext.BackingIndices = + BackingIndices.IndicesInvolved(originIndices, Set(ClusterIndexName.Local.wildcard)) + + override def dataStreamsFrom(request: ModifyDataStreamsAction.Request): Set[domain.DataStreamName] = { + request.getActions.asSafeList.map(_.getDataStream).flatMap(DataStreamName.fromString).toSet } - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala index 2440d94e68..e7b6b43798 100644 --- a/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala +++ b/es81x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala @@ -16,37 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.PromoteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class PromoteDataStreamEsRequestContext(actionRequest: PromoteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: PromoteDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: PromoteDataStreamAction.Request): Set[domain.DataStreamName] = + originDataStreams - override def update(request: PromoteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override def backingIndicesFrom(request: PromoteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 2e6a465c6b..cb0278c6c5 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -164,19 +164,19 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) // data streams case request: CreateDataStreamAction.Request => - regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: DataStreamsStatsAction.Request => - regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, clusterService, threadPool)) case request: DeleteDataStreamAction.Request => - regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: GetDataStreamAction.Request => - regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: MigrateToDataStreamAction.Request => - regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: ModifyDataStreamsAction.Request => - regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) case request: PromoteDataStreamAction.Request => - regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) // indices case request: GetIndexRequest => regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index fdf50a2563..b8abe1ff7a 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala index 4eb15cc219..65fd983790 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala @@ -16,38 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.CreateDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class CreateDataStreamEsRequestContext(actionRequest: CreateDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: CreateDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: CreateDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams - override def update(request: CreateDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: CreateDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala index c6a7dbdef1..6cf03d9301 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala @@ -16,12 +16,12 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DataStreamsStatsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +29,17 @@ import tech.beshu.ror.utils.ScalaOps._ class DataStreamsStatsEsRequestContext(actionRequest: DataStreamsStatsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DataStreamsStatsAction.Request): Set[domain.ClusterIndexName] = { - getIndexFrom(request).toSet - } + override def backingIndicesFrom(request: DataStreamsStatsAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DataStreamsStatsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified - } + override def dataStreamsFrom(request: DataStreamsStatsAction.Request): Set[domain.DataStreamName] = + actionRequest.indices().asSafeList.flatMap(DataStreamName.fromString).toSet - private def getIndexFrom(request: DataStreamsStatsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + actionRequest.indices(blockContext.dataStreams.map(DataStreamName.toString).toList: _*) + ModificationResult.Modified } } \ No newline at end of file diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala index adb3bd0d82..09f25f3b35 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala @@ -16,12 +16,11 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DeleteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +28,22 @@ import tech.beshu.ror.utils.ScalaOps._ class DeleteDataStreamEsRequestContext(actionRequest: DeleteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DeleteDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet - } + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: DeleteDataStreamAction.Request): Set[DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: DeleteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DeleteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) ModificationResult.Modified } - private def getIndicesFrom(request: DeleteDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def setDataStreamNames(dataStreams: Set[DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams } } \ No newline at end of file diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala index 9e5a94f234..bed3330b45 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala @@ -16,36 +16,84 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList +import monix.eval.Task import org.elasticsearch.action.datastreams.GetDataStreamAction +import org.elasticsearch.action.datastreams.GetDataStreamAction.Response +import org.elasticsearch.index.Index import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult import tech.beshu.ror.utils.ScalaOps._ +import scala.collection.JavaConverters._ + class GetDataStreamEsRequestContext(actionRequest: GetDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: GetDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: GetDataStreamAction.Request): BackingIndices = BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) + ModificationResult.UpdateResponse { + case r: GetDataStreamAction.Response => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + Task.now(updateGetDataStreamResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } + + private def setDataStreamNames(dataStreams: Set[domain.DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams + } - override def indicesFrom(request: GetDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private def updateGetDataStreamResponse(response: GetDataStreamAction.Response, + allAllowedIndices: Set[ClusterIndexName]): GetDataStreamAction.Response = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredStreams = + response + .getDataStreams.asSafeList + .filter { dataStreamInfo: Response.DataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + new GetDataStreamAction.Response(filteredStreams.asJava) } - override def update(request: GetDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified + private def backingIndiesMatchesAllowedIndices(info: Response.DataStreamInfo, allowedIndicesMatcher: Matcher[ClusterIndexName]) = { + val dataStreamIndices: Set[ClusterIndexName] = indicesFrom(info).keySet + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty } - private def getIndicesFrom(request: GetDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def indicesFrom(response: Response.DataStreamInfo): Map[ClusterIndexName, Index] = { + response + .getDataStream + .getIndices + .asSafeList + .flatMap { index => + Option(index.getName) + .flatMap(ClusterIndexName.fromString) + .map(clusterIndexName => (clusterIndexName, index)) + } + .toMap } } diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala index 796cc63030..8d458c8f42 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala @@ -16,37 +16,31 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.MigrateToDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class MigrateToDataStreamEsRequestContext(actionRequest: MigrateToDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originIndices = Option(actionRequest.getAliasName).flatMap(ClusterIndexName.fromString).toSet - override def indicesFrom(request: MigrateToDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: MigrateToDataStreamAction.Request): Set[domain.DataStreamName] = Set.empty - override def update(request: MigrateToDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with indices requires the same set of indices after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: MigrateToDataStreamAction.Request): BackingIndices = { + BackingIndices.IndicesInvolved(filteredIndices = originIndices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + } + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala index a91662a648..bb17ff137c 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala @@ -16,13 +16,13 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.ModifyDataStreamsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -30,24 +30,22 @@ import tech.beshu.ror.utils.ScalaOps._ class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet } - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted + override protected def backingIndicesFrom(request: ModifyDataStreamsAction.Request): DataStreamRequestBlockContext.BackingIndices = + BackingIndices.IndicesInvolved(originIndices, Set(ClusterIndexName.Local.wildcard)) + + override def dataStreamsFrom(request: ModifyDataStreamsAction.Request): Set[domain.DataStreamName] = { + request.getActions.asSafeList.map(_.getDataStream).flatMap(DataStreamName.fromString).toSet } - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala index 2440d94e68..e7b6b43798 100644 --- a/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala +++ b/es82x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala @@ -16,37 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.PromoteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class PromoteDataStreamEsRequestContext(actionRequest: PromoteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: PromoteDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: PromoteDataStreamAction.Request): Set[domain.DataStreamName] = + originDataStreams - override def update(request: PromoteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override def backingIndicesFrom(request: PromoteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 2e6a465c6b..cb0278c6c5 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -164,19 +164,19 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) // data streams case request: CreateDataStreamAction.Request => - regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: DataStreamsStatsAction.Request => - regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, clusterService, threadPool)) case request: DeleteDataStreamAction.Request => - regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: GetDataStreamAction.Request => - regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: MigrateToDataStreamAction.Request => - regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: ModifyDataStreamsAction.Request => - regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) case request: PromoteDataStreamAction.Request => - regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) // indices case request: GetIndexRequest => regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index fdf50a2563..b8abe1ff7a 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala index 4eb15cc219..65fd983790 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala @@ -16,38 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.CreateDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class CreateDataStreamEsRequestContext(actionRequest: CreateDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: CreateDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: CreateDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams - override def update(request: CreateDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: CreateDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala index c6a7dbdef1..6cf03d9301 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala @@ -16,12 +16,12 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DataStreamsStatsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +29,17 @@ import tech.beshu.ror.utils.ScalaOps._ class DataStreamsStatsEsRequestContext(actionRequest: DataStreamsStatsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DataStreamsStatsAction.Request): Set[domain.ClusterIndexName] = { - getIndexFrom(request).toSet - } + override def backingIndicesFrom(request: DataStreamsStatsAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DataStreamsStatsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified - } + override def dataStreamsFrom(request: DataStreamsStatsAction.Request): Set[domain.DataStreamName] = + actionRequest.indices().asSafeList.flatMap(DataStreamName.fromString).toSet - private def getIndexFrom(request: DataStreamsStatsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + actionRequest.indices(blockContext.dataStreams.map(DataStreamName.toString).toList: _*) + ModificationResult.Modified } } \ No newline at end of file diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala index adb3bd0d82..09f25f3b35 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala @@ -16,12 +16,11 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DeleteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +28,22 @@ import tech.beshu.ror.utils.ScalaOps._ class DeleteDataStreamEsRequestContext(actionRequest: DeleteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DeleteDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet - } + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: DeleteDataStreamAction.Request): Set[DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: DeleteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DeleteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) ModificationResult.Modified } - private def getIndicesFrom(request: DeleteDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def setDataStreamNames(dataStreams: Set[DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams } } \ No newline at end of file diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala index 9e5a94f234..bed3330b45 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala @@ -16,36 +16,84 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList +import monix.eval.Task import org.elasticsearch.action.datastreams.GetDataStreamAction +import org.elasticsearch.action.datastreams.GetDataStreamAction.Response +import org.elasticsearch.index.Index import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult import tech.beshu.ror.utils.ScalaOps._ +import scala.collection.JavaConverters._ + class GetDataStreamEsRequestContext(actionRequest: GetDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: GetDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: GetDataStreamAction.Request): BackingIndices = BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) + ModificationResult.UpdateResponse { + case r: GetDataStreamAction.Response => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + Task.now(updateGetDataStreamResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } + + private def setDataStreamNames(dataStreams: Set[domain.DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams + } - override def indicesFrom(request: GetDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private def updateGetDataStreamResponse(response: GetDataStreamAction.Response, + allAllowedIndices: Set[ClusterIndexName]): GetDataStreamAction.Response = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredStreams = + response + .getDataStreams.asSafeList + .filter { dataStreamInfo: Response.DataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + new GetDataStreamAction.Response(filteredStreams.asJava) } - override def update(request: GetDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified + private def backingIndiesMatchesAllowedIndices(info: Response.DataStreamInfo, allowedIndicesMatcher: Matcher[ClusterIndexName]) = { + val dataStreamIndices: Set[ClusterIndexName] = indicesFrom(info).keySet + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty } - private def getIndicesFrom(request: GetDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def indicesFrom(response: Response.DataStreamInfo): Map[ClusterIndexName, Index] = { + response + .getDataStream + .getIndices + .asSafeList + .flatMap { index => + Option(index.getName) + .flatMap(ClusterIndexName.fromString) + .map(clusterIndexName => (clusterIndexName, index)) + } + .toMap } } diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala index 796cc63030..8d458c8f42 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala @@ -16,37 +16,31 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.MigrateToDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class MigrateToDataStreamEsRequestContext(actionRequest: MigrateToDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originIndices = Option(actionRequest.getAliasName).flatMap(ClusterIndexName.fromString).toSet - override def indicesFrom(request: MigrateToDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: MigrateToDataStreamAction.Request): Set[domain.DataStreamName] = Set.empty - override def update(request: MigrateToDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with indices requires the same set of indices after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: MigrateToDataStreamAction.Request): BackingIndices = { + BackingIndices.IndicesInvolved(filteredIndices = originIndices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + } + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala index a91662a648..bb17ff137c 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala @@ -16,13 +16,13 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.ModifyDataStreamsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -30,24 +30,22 @@ import tech.beshu.ror.utils.ScalaOps._ class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet } - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted + override protected def backingIndicesFrom(request: ModifyDataStreamsAction.Request): DataStreamRequestBlockContext.BackingIndices = + BackingIndices.IndicesInvolved(originIndices, Set(ClusterIndexName.Local.wildcard)) + + override def dataStreamsFrom(request: ModifyDataStreamsAction.Request): Set[domain.DataStreamName] = { + request.getActions.asSafeList.map(_.getDataStream).flatMap(DataStreamName.fromString).toSet } - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala index 2440d94e68..e7b6b43798 100644 --- a/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala +++ b/es83x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala @@ -16,37 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.PromoteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class PromoteDataStreamEsRequestContext(actionRequest: PromoteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: PromoteDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: PromoteDataStreamAction.Request): Set[domain.DataStreamName] = + originDataStreams - override def update(request: PromoteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override def backingIndicesFrom(request: PromoteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 2e6a465c6b..cb0278c6c5 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -164,19 +164,19 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) // data streams case request: CreateDataStreamAction.Request => - regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: DataStreamsStatsAction.Request => - regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, clusterService, threadPool)) case request: DeleteDataStreamAction.Request => - regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: GetDataStreamAction.Request => - regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: MigrateToDataStreamAction.Request => - regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: ModifyDataStreamsAction.Request => - regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) case request: PromoteDataStreamAction.Request => - regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) // indices case request: GetIndexRequest => regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index fdf50a2563..b8abe1ff7a 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala index 4eb15cc219..65fd983790 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala @@ -16,38 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.CreateDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class CreateDataStreamEsRequestContext(actionRequest: CreateDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: CreateDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: CreateDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams - override def update(request: CreateDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: CreateDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala index c6a7dbdef1..6cf03d9301 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala @@ -16,12 +16,12 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DataStreamsStatsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +29,17 @@ import tech.beshu.ror.utils.ScalaOps._ class DataStreamsStatsEsRequestContext(actionRequest: DataStreamsStatsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DataStreamsStatsAction.Request): Set[domain.ClusterIndexName] = { - getIndexFrom(request).toSet - } + override def backingIndicesFrom(request: DataStreamsStatsAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DataStreamsStatsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified - } + override def dataStreamsFrom(request: DataStreamsStatsAction.Request): Set[domain.DataStreamName] = + actionRequest.indices().asSafeList.flatMap(DataStreamName.fromString).toSet - private def getIndexFrom(request: DataStreamsStatsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + actionRequest.indices(blockContext.dataStreams.map(DataStreamName.toString).toList: _*) + ModificationResult.Modified } } \ No newline at end of file diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala index adb3bd0d82..09f25f3b35 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala @@ -16,12 +16,11 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DeleteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +28,22 @@ import tech.beshu.ror.utils.ScalaOps._ class DeleteDataStreamEsRequestContext(actionRequest: DeleteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DeleteDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet - } + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: DeleteDataStreamAction.Request): Set[DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: DeleteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DeleteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) ModificationResult.Modified } - private def getIndicesFrom(request: DeleteDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def setDataStreamNames(dataStreams: Set[DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams } } \ No newline at end of file diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala index 9e5a94f234..bed3330b45 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala @@ -16,36 +16,84 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList +import monix.eval.Task import org.elasticsearch.action.datastreams.GetDataStreamAction +import org.elasticsearch.action.datastreams.GetDataStreamAction.Response +import org.elasticsearch.index.Index import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult import tech.beshu.ror.utils.ScalaOps._ +import scala.collection.JavaConverters._ + class GetDataStreamEsRequestContext(actionRequest: GetDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: GetDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: GetDataStreamAction.Request): BackingIndices = BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) + ModificationResult.UpdateResponse { + case r: GetDataStreamAction.Response => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + Task.now(updateGetDataStreamResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } + + private def setDataStreamNames(dataStreams: Set[domain.DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams + } - override def indicesFrom(request: GetDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private def updateGetDataStreamResponse(response: GetDataStreamAction.Response, + allAllowedIndices: Set[ClusterIndexName]): GetDataStreamAction.Response = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredStreams = + response + .getDataStreams.asSafeList + .filter { dataStreamInfo: Response.DataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + new GetDataStreamAction.Response(filteredStreams.asJava) } - override def update(request: GetDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified + private def backingIndiesMatchesAllowedIndices(info: Response.DataStreamInfo, allowedIndicesMatcher: Matcher[ClusterIndexName]) = { + val dataStreamIndices: Set[ClusterIndexName] = indicesFrom(info).keySet + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty } - private def getIndicesFrom(request: GetDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def indicesFrom(response: Response.DataStreamInfo): Map[ClusterIndexName, Index] = { + response + .getDataStream + .getIndices + .asSafeList + .flatMap { index => + Option(index.getName) + .flatMap(ClusterIndexName.fromString) + .map(clusterIndexName => (clusterIndexName, index)) + } + .toMap } } diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala index 796cc63030..8d458c8f42 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala @@ -16,37 +16,31 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.MigrateToDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class MigrateToDataStreamEsRequestContext(actionRequest: MigrateToDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originIndices = Option(actionRequest.getAliasName).flatMap(ClusterIndexName.fromString).toSet - override def indicesFrom(request: MigrateToDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: MigrateToDataStreamAction.Request): Set[domain.DataStreamName] = Set.empty - override def update(request: MigrateToDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with indices requires the same set of indices after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: MigrateToDataStreamAction.Request): BackingIndices = { + BackingIndices.IndicesInvolved(filteredIndices = originIndices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + } + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala index a91662a648..bb17ff137c 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala @@ -16,13 +16,13 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.ModifyDataStreamsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -30,24 +30,22 @@ import tech.beshu.ror.utils.ScalaOps._ class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet } - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted + override protected def backingIndicesFrom(request: ModifyDataStreamsAction.Request): DataStreamRequestBlockContext.BackingIndices = + BackingIndices.IndicesInvolved(originIndices, Set(ClusterIndexName.Local.wildcard)) + + override def dataStreamsFrom(request: ModifyDataStreamsAction.Request): Set[domain.DataStreamName] = { + request.getActions.asSafeList.map(_.getDataStream).flatMap(DataStreamName.fromString).toSet } - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala index 2440d94e68..e7b6b43798 100644 --- a/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala +++ b/es84x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala @@ -16,37 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.PromoteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class PromoteDataStreamEsRequestContext(actionRequest: PromoteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: PromoteDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: PromoteDataStreamAction.Request): Set[domain.DataStreamName] = + originDataStreams - override def update(request: PromoteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override def backingIndicesFrom(request: PromoteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala index 2e6a465c6b..cb0278c6c5 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -164,19 +164,19 @@ class AclAwareRequestFilter(clusterService: RorClusterService, regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) // data streams case request: CreateDataStreamAction.Request => - regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: DataStreamsStatsAction.Request => - regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, clusterService, threadPool)) case request: DeleteDataStreamAction.Request => - regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: GetDataStreamAction.Request => - regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: MigrateToDataStreamAction.Request => - regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) case request: ModifyDataStreamsAction.Request => - regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) case request: PromoteDataStreamAction.Request => - regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) // indices case request: GetIndexRequest => regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala index b455cfdec6..0270d630c2 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -124,6 +124,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -142,6 +143,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | TemplateRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => @@ -160,6 +162,7 @@ class RegularRequestHandler(engine: Engine, GeneralNonIndexRequestBlockContextUpdater | RepositoryRequestBlockContextUpdater | SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | AliasRequestBlockContextUpdater | MultiIndexRequestBlockContextUpdater | RorApiRequestBlockContextUpdater => diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala index fdf50a2563..b8abe1ff7a 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -143,4 +143,8 @@ abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, protected def snapshotsOrWildcard(snapshots: Set[SnapshotName]): Set[SnapshotName] = { if (snapshots.nonEmpty) snapshots else Set(SnapshotName.all) } + + protected def dataStreamsOrWildcard(dataStreams: Set[DataStreamName]): Set[DataStreamName] = { + if (dataStreams.nonEmpty) dataStreams else Set(DataStreamName.all) + } } \ No newline at end of file diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..d54e0547ac --- /dev/null +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala @@ -0,0 +1,62 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.es.handler.request.context.types + +import cats.implicits._ +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.DataStreamName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest} + +abstract class BaseDataStreamsEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[DataStreamRequestBlockContext](esContext, clusterService) + with EsRequest[DataStreamRequestBlockContext] { + + override val initialBlockContext: DataStreamRequestBlockContext = DataStreamRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + dataStreams = { + val dataStreams = dataStreamsOrWildcard(dataStreamsFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered data streams: ${dataStreams.map(_.show).mkString(",")}") + dataStreams + }, + backingIndices = { + val backingIndices = backingIndicesFrom(actionRequest) + backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + logger.debug(s"[${id.show}] Discovered indices: ${filteredIndices.map(_.show).mkString(",")}") + case BackingIndices.IndicesNotInvolved => + } + backingIndices + }, + ) + + protected def dataStreamsFrom(request: R): Set[DataStreamName] + + protected def backingIndicesFrom(request: R): BackingIndices + +} diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala index 4eb15cc219..65fd983790 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/CreateDataStreamEsRequestContext.scala @@ -16,38 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.CreateDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class CreateDataStreamEsRequestContext(actionRequest: CreateDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: CreateDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: CreateDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams - override def update(request: CreateDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: CreateDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala index c6a7dbdef1..6cf03d9301 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DataStreamsStatsEsRequestContext.scala @@ -16,12 +16,12 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DataStreamsStatsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +29,17 @@ import tech.beshu.ror.utils.ScalaOps._ class DataStreamsStatsEsRequestContext(actionRequest: DataStreamsStatsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DataStreamsStatsAction.Request): Set[domain.ClusterIndexName] = { - getIndexFrom(request).toSet - } + override def backingIndicesFrom(request: DataStreamsStatsAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DataStreamsStatsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified - } + override def dataStreamsFrom(request: DataStreamsStatsAction.Request): Set[domain.DataStreamName] = + actionRequest.indices().asSafeList.flatMap(DataStreamName.fromString).toSet - private def getIndexFrom(request: DataStreamsStatsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + actionRequest.indices(blockContext.dataStreams.map(DataStreamName.toString).toList: _*) + ModificationResult.Modified } } \ No newline at end of file diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala index adb3bd0d82..09f25f3b35 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDataStreamEsRequestContext.scala @@ -16,12 +16,11 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList import org.elasticsearch.action.datastreams.DeleteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext -import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -29,23 +28,22 @@ import tech.beshu.ror.utils.ScalaOps._ class DeleteDataStreamEsRequestContext(actionRequest: DeleteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - override def indicesFrom(request: DeleteDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet - } + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: DeleteDataStreamAction.Request): Set[DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: DeleteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved - override def update(request: DeleteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) + override protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) ModificationResult.Modified } - private def getIndicesFrom(request: DeleteDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def setDataStreamNames(dataStreams: Set[DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams } } \ No newline at end of file diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala index 9e5a94f234..bed3330b45 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetDataStreamEsRequestContext.scala @@ -16,36 +16,84 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList +import monix.eval.Task import org.elasticsearch.action.datastreams.GetDataStreamAction +import org.elasticsearch.action.datastreams.GetDataStreamAction.Response +import org.elasticsearch.index.Index import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.{Matcher, MatcherWithWildcardsScalaAdapter} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult import tech.beshu.ror.utils.ScalaOps._ +import scala.collection.JavaConverters._ + class GetDataStreamEsRequestContext(actionRequest: GetDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originDataStreams = actionRequest.getNames.asSafeList.flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: GetDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams + + override protected def backingIndicesFrom(request: GetDataStreamAction.Request): BackingIndices = BackingIndices.IndicesInvolved( + filteredIndices = Set.empty, allAllowedIndices = Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) + ModificationResult.UpdateResponse { + case r: GetDataStreamAction.Response => + blockContext.backingIndices match { + case BackingIndices.IndicesInvolved(filteredIndices, allAllowedIndices) => + Task.now(updateGetDataStreamResponse(r, allAllowedIndices)) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } + + private def setDataStreamNames(dataStreams: Set[domain.DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams + } - override def indicesFrom(request: GetDataStreamAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private def updateGetDataStreamResponse(response: GetDataStreamAction.Response, + allAllowedIndices: Set[ClusterIndexName]): GetDataStreamAction.Response = { + val allowedIndicesMatcher = MatcherWithWildcardsScalaAdapter.create(allAllowedIndices) + val filteredStreams = + response + .getDataStreams.asSafeList + .filter { dataStreamInfo: Response.DataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + new GetDataStreamAction.Response(filteredStreams.asJava) } - override def update(request: GetDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - request.indices(filteredIndices.map(_.stringify).toList: _*) - ModificationResult.Modified + private def backingIndiesMatchesAllowedIndices(info: Response.DataStreamInfo, allowedIndicesMatcher: Matcher[ClusterIndexName]) = { + val dataStreamIndices: Set[ClusterIndexName] = indicesFrom(info).keySet + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty } - private def getIndicesFrom(request: GetDataStreamAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + private def indicesFrom(response: Response.DataStreamInfo): Map[ClusterIndexName, Index] = { + response + .getDataStream + .getIndices + .asSafeList + .flatMap { index => + Option(index.getName) + .flatMap(ClusterIndexName.fromString) + .map(clusterIndexName => (clusterIndexName, index)) + } + .toMap } } diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala index 796cc63030..8d458c8f42 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MigrateToDataStreamEsRequestContext.scala @@ -16,37 +16,31 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.MigrateToDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.ClusterIndexName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class MigrateToDataStreamEsRequestContext(actionRequest: MigrateToDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originIndices = Option(actionRequest.getAliasName).flatMap(ClusterIndexName.fromString).toSet - override def indicesFrom(request: MigrateToDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: MigrateToDataStreamAction.Request): Set[domain.DataStreamName] = Set.empty - override def update(request: MigrateToDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with indices requires the same set of indices after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override protected def backingIndicesFrom(request: MigrateToDataStreamAction.Request): BackingIndices = { + BackingIndices.IndicesInvolved(filteredIndices = originIndices, allAllowedIndices = Set(ClusterIndexName.Local.wildcard)) + } + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala index cda017a916..bb17ff137c 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ModifyDataStreamsEsRequestContext.scala @@ -16,13 +16,13 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.ModifyDataStreamsAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult @@ -30,26 +30,22 @@ import tech.beshu.ror.utils.ScalaOps._ class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = getIndicesFrom(actionRequest).toSet - - override def indicesFrom(request: ModifyDataStreamsAction.Request): Set[domain.ClusterIndexName] = { - getIndicesFrom(request).toSet + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet } - override def update(request: ModifyDataStreamsAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - logger.error(s"[${id.show}] This request ${request.getClass.getCanonicalName} cannot be handled by the ROR ACL, " + - s"so it's forbidden for security reasons. Please report the issue.") - ModificationResult.ShouldBeInterrupted + override protected def backingIndicesFrom(request: ModifyDataStreamsAction.Request): DataStreamRequestBlockContext.BackingIndices = + BackingIndices.IndicesInvolved(originIndices, Set(ClusterIndexName.Local.wildcard)) + + override def dataStreamsFrom(request: ModifyDataStreamsAction.Request): Set[domain.DataStreamName] = { + request.getActions.asSafeList.map(_.getDataStream).flatMap(DataStreamName.fromString).toSet } - private def getIndicesFrom(request: ModifyDataStreamsAction.Request) = { - request.indices().asSafeList.flatMap(ClusterIndexName.fromString) + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL } } diff --git a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala index 2440d94e68..e7b6b43798 100644 --- a/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala +++ b/es85x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PromoteDataStreamEsRequestContext.scala @@ -16,37 +16,30 @@ */ package tech.beshu.ror.es.handler.request.context.types -import cats.data.NonEmptyList -import cats.implicits._ import org.elasticsearch.action.datastreams.PromoteDataStreamAction import org.elasticsearch.threadpool.ThreadPool -import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain.DataStreamName import tech.beshu.ror.es.RorClusterService import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext import tech.beshu.ror.es.handler.request.context.ModificationResult -import tech.beshu.ror.utils.ScalaOps._ class PromoteDataStreamEsRequestContext(actionRequest: PromoteDataStreamAction.Request, esContext: EsContext, - aclContext: AccessControlStaticContext, clusterService: RorClusterService, override val threadPool: ThreadPool) - extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { - private lazy val originIndices = actionRequest.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet - override def indicesFrom(request: PromoteDataStreamAction.Request): Set[domain.ClusterIndexName] = originIndices + override protected def dataStreamsFrom(request: PromoteDataStreamAction.Request): Set[domain.DataStreamName] = + originDataStreams - override def update(request: PromoteDataStreamAction.Request, - filteredIndices: NonEmptyList[ClusterIndexName], - allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { - if (originIndices == filteredIndices.toList.toSet) { - ModificationResult.Modified - } else { - logger.error(s"[${id.show}] Write request with data streams requires the same set of data streams after filtering as at the beginning. Please report the issue.") - ModificationResult.ShouldBeInterrupted - } + override def backingIndicesFrom(request: PromoteDataStreamAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream already processed by ACL } } diff --git a/gradle.properties b/gradle.properties index e515a6af36..e14206a850 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ publishedPluginVersion=1.46.0 -pluginVersion=1.47.0-pre2 +pluginVersion=1.47.0-pre3 pluginName=readonlyrest \ No newline at end of file diff --git a/integration-tests/src/test/resources/data_stream_api/readonlyrest.yml b/integration-tests/src/test/resources/data_stream_api/readonlyrest.yml index 2d39c99924..dad5517a83 100644 --- a/integration-tests/src/test/resources/data_stream_api/readonlyrest.yml +++ b/integration-tests/src/test/resources/data_stream_api/readonlyrest.yml @@ -8,7 +8,7 @@ readonlyrest: - name: "User1 access streams" auth_key: "user1:pass" - indices: + data_streams: - 'data-stream-test*' actions: - indices:admin/data_stream/* @@ -18,16 +18,15 @@ readonlyrest: - name: "User2 access streams" auth_key: "user2:pass" - indices: - - 'data-stream-dev*' - - 'data-stream-test*' + data_streams: + - 'data-stream-prod' actions: - indices:admin/data_stream/* - indices:monitor/data_stream/* - indices:data/read/* - cluster:monitor/main - - name: "User3 access streams index" + - name: "User3 access indices" auth_key: "user3:pass" indices: - '.ds-data-stream-dev*' @@ -38,7 +37,7 @@ readonlyrest: - indices:data/read/* - cluster:monitor/main - - name: "User4 access streams" + - name: "User4 access indices" auth_key: "user4:pass" indices: - '*' @@ -47,3 +46,25 @@ readonlyrest: - indices:monitor/data_stream/* - indices:data/read/* - cluster:monitor/main + + - name: "User5 access indices" + auth_key: "user5:pass" + indices: + - 'data-stream-dev*' + - 'data-stream-test*' + actions: + - indices:admin/data_stream/* + - indices:monitor/data_stream/* + - indices:data/read/* + - cluster:monitor/main + + - name: "User6 access streams" + auth_key: "user5:pass" + data_streams: + - 'data-stream-dev*' + - 'data-stream-test*' + actions: + - indices:admin/data_stream/* + - indices:monitor/data_stream/* + - indices:data/read/* + - cluster:monitor/main \ No newline at end of file diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/plugin/DataStreamApiSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/plugin/DataStreamApiSuite.scala deleted file mode 100644 index 99a736c525..0000000000 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/plugin/DataStreamApiSuite.scala +++ /dev/null @@ -1,571 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.integration.plugin - -import monix.execution.atomic.Atomic -import org.scalatest.BeforeAndAfterEach -import org.scalatest.freespec.AnyFreeSpec -import tech.beshu.ror.integration.plugin.DataStreamApiSuite.{DataStreamNameGenerator, IndexTemplateNameGenerator} -import tech.beshu.ror.integration.suites.base.support.BaseSingleNodeEsClusterTest -import tech.beshu.ror.integration.utils.ESVersionSupportForAnyFreeSpecLike -import tech.beshu.ror.utils.containers.EsClusterProvider -import tech.beshu.ror.utils.elasticsearch._ -import tech.beshu.ror.utils.misc.Version - -import java.time.Instant -import scala.util.Random - -trait DataStreamApiSuite extends AnyFreeSpec - with BaseSingleNodeEsClusterTest - with ESVersionSupportForAnyFreeSpecLike - with BeforeAndAfterEach { - this: EsClusterProvider => - - override implicit val rorConfigFileName: String = "/data_stream_api/readonlyrest.yml" - - private lazy val client = clients.head.adminClient - private lazy val user1Client = clients.head.basicAuthClient("user1", "pass") - private lazy val adminDocumentManager = new DocumentManager(client, esVersionUsed) - private lazy val adminDataStreamManager = new DataStreamManager(client) - private lazy val adminIndexManager = new IndexManager(client, esVersionUsed) - private lazy val adminSearchManager = new SearchManager(client) - private lazy val adminTemplateManager = new IndexTemplateManager(client, esVersionUsed) - - private val adminDataStream = DataStreamNameGenerator.next("admin") - private val devDataStream = DataStreamNameGenerator.next("dev") - private val testDataStream = DataStreamNameGenerator.next("test") - - "Data stream API" - { - "Search API" - { - "without indices rule should" - { - "allow to search by data stream name" excludeES(allEs6x, allEs7xBelowEs79x) in { - createDataStream(adminDataStream, IndexTemplateNameGenerator.next) - createDocsInDataStream(adminDataStream, 2) - - val searchResponse = adminSearchManager.search(adminDataStream) - searchResponse.totalHits should be(2) - } - "allow to search by data stream name with wildcard" excludeES(allEs6x, allEs7xBelowEs79x) in { - adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(adminDataStream)).force() - - List( - s"$adminDataStream-x0", - s"$adminDataStream-x1", - s"$adminDataStream-x2", - s"$adminDataStream-x10", - s"$adminDataStream-x11", - ).foreach { dataStream => - adminDataStreamManager.createDataStream(dataStream).force() - createDocsInDataStream(dataStream, 1) - } - - List( - ("data-stream*", 5), - (s"$adminDataStream*", 5), - (s"$adminDataStream-x1*", 3), - ) - .foreach { case (dataStream, expectedHits) => - val response = adminSearchManager.searchAll(dataStream) - response.totalHits should be(expectedHits) - } - } - "allow to search by data stream index name" excludeES(allEs6x, allEs7xBelowEs79x) in { - createDataStream(adminDataStream, IndexTemplateNameGenerator.next) - createDocsInDataStream(adminDataStream, 1) - adminIndexManager.rollover(adminDataStream).force() - createDocsInDataStream(adminDataStream, 1) - adminIndexManager.rollover(adminDataStream).force() - createDocsInDataStream(adminDataStream, 1) - - val allDataStreamsResponse = adminDataStreamManager.getAllDataStreams().force() - val indicesNames = allDataStreamsResponse.responseJson("data_streams").arr.head("indices").arr.map(v => v("index_name").str) - indicesNames.foreach { indexName => - val response = adminSearchManager.searchAll(indexName) - response.totalHits should be(1) - } - } - "allow to search by data stream index with wildcard" excludeES(allEs6x, allEs7xBelowEs79x) in { - createDataStream(adminDataStream, IndexTemplateNameGenerator.next) - createDocsInDataStream(adminDataStream, 1) - adminIndexManager.rollover(adminDataStream) - createDocsInDataStream(adminDataStream, 1) - adminIndexManager.rollover(adminDataStream) - createDocsInDataStream(adminDataStream, 1) - - List( - (".ds-data-stream*", 3), - (s".ds-$adminDataStream*", 3), - ) - .foreach { case (dataStream, expectedHits) => - val response = adminSearchManager.searchAll(dataStream) - response.totalHits should be(expectedHits) - } - } - } - "with indices rule should" - { - "allow to search by data stream name" excludeES(allEs6x, allEs7xBelowEs79x) in { - List( - (adminDataStream, 4), - (devDataStream, 2), - (testDataStream, 1) - ) - .foreach { case (dataStream, docsCount) => - createDataStream(dataStream, IndexTemplateNameGenerator.next) - createDocsInDataStream(dataStream, docsCount) - } - - val searchManager1 = new SearchManager(user1Client) - - List(devDataStream, adminDataStream).foreach { dataStream => - val response = searchManager1.searchAll(dataStream) - response.responseCode should be(401) - } - - val sm1TestDataStreamResponse = searchManager1.searchAll(testDataStream) - sm1TestDataStreamResponse.totalHits should be(1) - - val searchManager2 = new SearchManager(clients.head.basicAuthClient("user2", "pass")) - val sm2Response1 = searchManager2.searchAll(devDataStream) - sm2Response1.totalHits should be(2) - val sm2Response2 = searchManager2.searchAll(testDataStream) - sm2Response2.totalHits should be(1) - val sm2Response3 = searchManager2.searchAll(adminDataStream) - sm2Response3.responseCode should be(401) - } - "allow to search by data stream index with wildcard" excludeES(allEs6x, allEs7xBelowEs79x) in { - List( - (adminDataStream, 4), - (devDataStream, 2), - (testDataStream, 1) - ) - .foreach { case (dataStream, docsCount) => - createDataStream(dataStream, IndexTemplateNameGenerator.next) - createDocsInDataStream(dataStream, docsCount) - } - - val searchManager = new SearchManager(clients.head.basicAuthClient("user3", "pass")) - - List( - (s".ds-$devDataStream*", 2), - (s".ds-$testDataStream*", 1), - (s".ds-$adminDataStream*", 0), - ) - .foreach { case (dataStream, expectedHits) => - val response = searchManager.searchAll(dataStream) - response.totalHits should be(expectedHits) - } - } - "allow to search by data stream index" excludeES(allEs6x, allEs7xBelowEs79x) in { - List( - (adminDataStream, 4), - (devDataStream, 2), - (testDataStream, 1) - ) - .foreach { case (dataStream, docsCount) => - createDataStream(dataStream, IndexTemplateNameGenerator.next) - createDocsInDataStream(dataStream, docsCount) - } - - val searchManager = new SearchManager(clients.head.basicAuthClient("user3", "pass")) - val dataStreamsJson = adminDataStreamManager.getAllDataStreams().force().responseJson("data_streams").arr - - def findIndicesForDataStream(name: String) = - dataStreamsJson - .find(v => v("name").str == name) - .map(v => v("indices").arr.map(v => v("index_name").str)) - - findIndicesForDataStream(adminDataStream).head.foreach { indexName => - val response = searchManager.searchAll(indexName) - response.responseCode should be(401) - } - findIndicesForDataStream(devDataStream).head.foreach { indexName => - val response = searchManager.searchAll(indexName) - response.totalHits should be(2) - } - findIndicesForDataStream(testDataStream).head.foreach { indexName => - val response = searchManager.searchAll(indexName) - response.totalHits should be(1) - } - } - } - } - "create data stream" - { - "without indices rule should" - { - "allow to create data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = adminDataStream - createDataStream(dataStream, IndexTemplateNameGenerator.next) - } - } - "with indices rule should" - { - "allow to create data stream when" - { - "the data stream name does match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("test") - adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() - - val dsm = new DataStreamManager(user1Client) - val response = dsm.createDataStream(dataStream) - response.responseCode should be(200) - } - } - "forbid to create data stream when" - { - "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() - - val dsm = new DataStreamManager(user1Client) - val response = dsm.createDataStream(dataStream) - response.responseCode should be(401) - } - "the index name does not match for data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("test") - adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() - - val dsm = new DataStreamManager(clients.head.basicAuthClient("user3", "pass")) - val response = dsm.createDataStream(dataStream) - response.responseCode should be(401) - } - } - } - } - "allow to add documents to data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - createDocsInDataStream(dataStream, 1) - } - "get all data streams" - { - "without indices rule should" - { - "allow to get all data streams" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream1 = DataStreamNameGenerator.next("admin") - val dataStream2 = DataStreamNameGenerator.next("dev") - - createDataStream(dataStream1, IndexTemplateNameGenerator.next) - createDataStream(dataStream2, IndexTemplateNameGenerator.next) - - val response = adminDataStreamManager.getAllDataStreams() - response.responseCode should be(200) - response.responseJson("data_streams").arr.map(v => v("name").str).toSet should be(Set(dataStream1, dataStream2)) - } - } - "with indices rule should" - { - "allow to get all data streams when" - { - "the user has access to all indices" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream1 = DataStreamNameGenerator.next("admin") - val dataStream2 = DataStreamNameGenerator.next("dev") - - createDataStream(dataStream1, IndexTemplateNameGenerator.next) - createDataStream(dataStream2, IndexTemplateNameGenerator.next) - - val dsm = new DataStreamManager(clients.head.basicAuthClient("user4", "pass")) - val response = dsm.getAllDataStreams() - response.responseCode should be(200) - response.responseJson("data_streams").arr.map(v => v("name").str).toSet should be(Set(dataStream1, dataStream2)) - } - } - "forbid to get all data streams when" - { - "the user has no access to all indices" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - val dsm = new DataStreamManager(user1Client) - val response = dsm.getAllDataStreams() - response.responseCode should be(401) - } - } - } - } - "get data stream" - { - "without indices rule should" - { - "allow to get data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - val response = adminDataStreamManager.getDataStream(dataStream) - response.responseCode should be(200) - response.responseJson("data_streams").arr.map(v => v("name").str).toList should be(List(dataStream)) - } - } - "with indices rule should" - { - "allow to get data stream when" - { - "the data stream name does match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("test") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - val dsm = new DataStreamManager(user1Client) - val response = dsm.getDataStream(dataStream) - response.responseCode should be(200) - response.responseJson("data_streams").arr.map(v => v("name").str).toSet should be(Set(dataStream)) - } - } - "forbid to get data stream when" - { - "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - val dsm = new DataStreamManager(user1Client) - val response = dsm.getDataStream(dataStream) - response.responseCode should be(401) - } - } - } - } - "get data stream stats" - { - "without indices rule should" - { - "allow to get data stream stats" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - val response = adminDataStreamManager.getDataStreamStats(dataStream) - response.responseCode should be(200) - response.responseJson("data_stream_count").num.toInt should be(1) - response.responseJson("backing_indices").num.toInt should be(1) - } - } - "with indices rule should" - { - "allow to get data stream stats when" - { - "the data stream name does match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("test") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - val dsm = new DataStreamManager(user1Client) - val response = dsm.getDataStreamStats(dataStream) - response.responseCode should be(200) - response.responseCode should be(200) - response.responseJson("data_stream_count").num.toInt should be(1) - response.responseJson("backing_indices").num.toInt should be(1) - } - } - "forbid to get data stream stats when" - { - "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - val dsm = new DataStreamManager(user1Client) - val response = dsm.getDataStreamStats(dataStream) - response.responseCode should be(401) - } - } - } - } - "should allow to rollover data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - List.range(0, 2).foreach { _ => - createDocsInDataStream(dataStream, 1) - adminIndexManager.rollover(dataStream).force() - } - - val statsResponse = adminDataStreamManager.getDataStreamStats(dataStream) - statsResponse.responseCode should be(200) - statsResponse.responseJson("data_stream_count").num.toInt should be(1) - statsResponse.responseJson("backing_indices").num.toInt should be(3) - } - "migrate index alias to data stream" - { - "without indices rule should" - { - "allow to migrate index alias to data stream" excludeES(allEs6x, allEs7xBelowEs711x) in { - val dataStream = DataStreamNameGenerator.next("admin") - adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test1", "@timestamp": "${format(Instant.now())}"}""")).force() - adminDocumentManager.createDoc("logs-0001", 2, ujson.read(s"""{ "message":"test2", "@timestamp": "${format(Instant.now())}"}""")).force() - adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test3", "@timestamp": "${format(Instant.now())}"}""")).force() - adminIndexManager.createAliasOf("logs-0001", dataStream).force() - - adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() - - val migrateToDataStreamResponse = adminDataStreamManager.migrateToDataStream(dataStream) - migrateToDataStreamResponse.responseCode should be(200) - - val statsResponse = adminDataStreamManager.getDataStreamStats(dataStream) - statsResponse.responseCode should be(200) - statsResponse.responseJson("data_stream_count").num.toInt should be(1) - statsResponse.responseJson("backing_indices").num.toInt should be(1) - } - } - "with indices rule should" - { - "allow to migrate index alias to data stream when" - { - "the data stream name does match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs711x) in { - val dataStream = DataStreamNameGenerator.next("test") - adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test1", "@timestamp": "${format(Instant.now())}"}""")).force() - adminDocumentManager.createDoc("logs-0001", 2, ujson.read(s"""{ "message":"test2", "@timestamp": "${format(Instant.now())}"}""")).force() - adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test3", "@timestamp": "${format(Instant.now())}"}""")).force() - adminIndexManager.createAliasOf("logs-0001", dataStream).force() - - adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() - - val dsm = new DataStreamManager(user1Client) - val migrateToDataStreamResponse = dsm.migrateToDataStream(dataStream) - migrateToDataStreamResponse.responseCode should be(200) - - val statsResponse = adminDataStreamManager.getDataStreamStats(dataStream) - statsResponse.responseCode should be(200) - statsResponse.responseJson("data_stream_count").num.toInt should be(1) - statsResponse.responseJson("backing_indices").num.toInt should be(1) - } - } - "forbid to migrate index alias to data stream when" - { - "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs711x) in { - val dataStream = DataStreamNameGenerator.next("admin") - adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test1", "@timestamp": "${format(Instant.now())}"}""")).force() - adminDocumentManager.createDoc("logs-0001", 2, ujson.read(s"""{ "message":"test2", "@timestamp": "${format(Instant.now())}"}""")).force() - adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test3", "@timestamp": "${format(Instant.now())}"}""")).force() - adminIndexManager.createAliasOf("logs-0001", dataStream).force() - - adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() - - val dsm = new DataStreamManager(user1Client) - val migrateToDataStreamResponse = dsm.migrateToDataStream(dataStream) - migrateToDataStreamResponse.responseCode should be(401) - } - } - } - } - "modify data stream" - { - "without indices rule should" - { - "forbid any attempt to modify data stream" excludeES(allEs6x, allEs7xBelowEs716x) in { - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - List.range(0, 2).foreach { _ => - createDocsInDataStream(dataStream, 1) - adminIndexManager.rollover(dataStream).force() - } - - val getDataStreamResponse = adminDataStreamManager.getDataStream(dataStream).force() - val dsIndices = getDataStreamResponse.responseJson("data_streams").arr.head("indices").arr.map(_ ("index_name").str).toList - dsIndices.size should be(3) - - val modifyResponse = adminDataStreamManager.modifyDataStreams(ujson.read( - s""" - |{ - | "actions": [ - | { - | "remove_backing_index": { - | "data_stream": "$dataStream", - | "index": "${dsIndices.head}" - | } - | } - | ] - |} - |""".stripMargin)) - modifyResponse.responseCode should be(401) - } - "allow to modify data stream" excludeES(allEs6x, allEs7xBelowEs716x) ignore { // enable when data stream rule will be introduced - val dataStream = DataStreamNameGenerator.next("admin") - createDataStream(dataStream, IndexTemplateNameGenerator.next) - - List.range(0, 2).foreach { _ => - createDocsInDataStream(dataStream, 1) - adminIndexManager.rollover(dataStream).force() - } - - val getDataStreamResponse = adminDataStreamManager.getDataStream(dataStream).force() - val dsIndices = getDataStreamResponse.responseJson("data_streams").arr.head("indices").arr.map(_ ("index_name").str).toList - dsIndices.size should be(3) - - val modifyResponse = adminDataStreamManager.modifyDataStreams(ujson.read( - s""" - |{ - | "actions": [ - | { - | "remove_backing_index": { - | "data_stream": "$dataStream", - | "index": "${dsIndices.head}" - | } - | } - | ] - |} - |""".stripMargin)) - modifyResponse.responseCode should be(200) - - val getDataStreamResponseAfterModification = adminDataStreamManager.getDataStream(dataStream) - getDataStreamResponseAfterModification.responseCode should be(200) - val dsIndicesAfterModification = getDataStreamResponseAfterModification - .responseJson("data_streams").arr.head("indices").arr.map(_ ("index_name").str).toList - dsIndicesAfterModification should be(dsIndices.tail) - } - } - } - } - - private def createDataStream(dataStreamName: String, indexTemplateName: String): Unit = { - val createIndexTemplateResponse = adminTemplateManager.createTemplate(indexTemplateName, indexTemplate(dataStreamName)) - createIndexTemplateResponse.responseCode should be(200) - val createDataStreamResponse = adminDataStreamManager.createDataStream(dataStreamName) - createDataStreamResponse.responseCode should be(200) - } - - private def createDocsInDataStream(streamName: String, count: Int): Unit = { - List.range(0, count).foreach { c => - val doc = ujson.read(s"""{ "message":"test$c", "@timestamp": "${format(Instant.now())}"}""") - adminDocumentManager.createDocWithGeneratedId(streamName, doc).force() - } - } - - private def indexTemplate(dataStreamName: String) = ujson.read( - s""" - |{ - | "index_patterns": ["$dataStreamName*"], - | "data_stream": { }, - | "priority": 500, - | "template": { - | "mappings": { - | "properties": { - | "@timestamp": { - | "type": "date", - | "format": "date_optional_time||epoch_millis" - | }, - | "message": { - | "type": "wildcard" - | } - | } - | } - | } - |} - |""".stripMargin - ) - - private def format(instant: Instant) = instant.toString - - override def beforeEach(): Unit = { - if (Version.greaterOrEqualThan(esVersionUsed, 7, 9, 0)) { - val dataStreamsResponse = adminDataStreamManager.getAllDataStreams().force() - dataStreamsResponse.responseJson("data_streams").arr.foreach { entry => - adminDataStreamManager.deleteDataStream(entry("name").str).force() - } - - adminTemplateManager - .getTemplates - .templates - .filter(_.name.startsWith("index-template")) - .foreach { template => - adminTemplateManager.deleteTemplate(template.name).force() - } - } - super.beforeEach() - } -} - -private object DataStreamApiSuite { - object IndexTemplateNameGenerator { - private val uniquePart = Atomic(0) - - def next: String = s"index-template-${uniquePart.incrementAndGet()}" - } - - object DataStreamNameGenerator { - def next(infix: String): String = s"data-stream-$infix-$randomSuffix" - - private def randomSuffix: String = Random.alphanumeric.take(12).mkString.toLowerCase - } -} diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/plugin/DataStreamsPluginTests.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/plugin/DataStreamsPluginTests.scala index c083546b47..cb54f0abe5 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/plugin/DataStreamsPluginTests.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/plugin/DataStreamsPluginTests.scala @@ -16,6 +16,7 @@ */ package tech.beshu.ror.integration.plugin +import tech.beshu.ror.integration.suites.DataStreamApiSuite import tech.beshu.ror.integration.utils.SingletonPluginTestSupport class DataStreamsPluginTests extends DataStreamApiSuite with SingletonPluginTestSupport diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/DataStreamApiSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/DataStreamApiSuite.scala new file mode 100644 index 0000000000..793ee453a7 --- /dev/null +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/DataStreamApiSuite.scala @@ -0,0 +1,887 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.integration.suites + +import monix.execution.atomic.Atomic +import org.scalatest.BeforeAndAfterEach +import org.scalatest.freespec.AnyFreeSpec +import tech.beshu.ror.integration.suites.DataStreamApiSuite.{DataStreamNameGenerator, IndexTemplateNameGenerator} +import tech.beshu.ror.integration.suites.base.support.BaseSingleNodeEsClusterTest +import tech.beshu.ror.integration.utils.ESVersionSupportForAnyFreeSpecLike +import tech.beshu.ror.utils.containers.EsClusterProvider +import tech.beshu.ror.utils.elasticsearch._ +import tech.beshu.ror.utils.misc.Version + +import java.time.Instant +import scala.util.Random + +trait DataStreamApiSuite extends AnyFreeSpec + with BaseSingleNodeEsClusterTest + with ESVersionSupportForAnyFreeSpecLike + with BeforeAndAfterEach { + this: EsClusterProvider => + + override implicit val rorConfigFileName: String = "/data_stream_api/readonlyrest.yml" + + private lazy val client = clients.head.adminClient + private lazy val user1Client = clients.head.basicAuthClient("user1", "pass") + private lazy val user2Client = clients.head.basicAuthClient("user2", "pass") + private lazy val user3Client = clients.head.basicAuthClient("user3", "pass") + private lazy val user4Client = clients.head.basicAuthClient("user4", "pass") + private lazy val user5Client = clients.head.basicAuthClient("user5", "pass") + private lazy val user6Client = clients.head.basicAuthClient("user6", "pass") + private lazy val adminDocumentManager = new DocumentManager(client, esVersionUsed) + private lazy val adminDataStreamManager = new DataStreamManager(client) + private lazy val adminIndexManager = new IndexManager(client, esVersionUsed) + private lazy val adminSearchManager = new SearchManager(client) + private lazy val adminTemplateManager = new IndexTemplateManager(client, esVersionUsed) + + private val adminDataStream = DataStreamNameGenerator.next("admin") + private val devDataStream = DataStreamNameGenerator.next("dev") + private val testDataStream = DataStreamNameGenerator.next("test") + + "Data stream API" - { + "Search API" - { + "without rules should" - { + "allow to search by data stream name" excludeES(allEs6x, allEs7xBelowEs79x) in { + createDataStream(adminDataStream, IndexTemplateNameGenerator.next) + createDocsInDataStream(adminDataStream, 2) + + val searchResponse = adminSearchManager.search(adminDataStream) + searchResponse.totalHits should be(2) + } + "allow to search by data stream name with wildcard" excludeES(allEs6x, allEs7xBelowEs79x) in { + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(adminDataStream)).force() + + List( + s"$adminDataStream-x0", + s"$adminDataStream-x1", + s"$adminDataStream-x2", + s"$adminDataStream-x10", + s"$adminDataStream-x11", + ).foreach { dataStream => + adminDataStreamManager.createDataStream(dataStream).force() + createDocsInDataStream(dataStream, 1) + } + + List( + ("data-stream*", 5), + (s"$adminDataStream*", 5), + (s"$adminDataStream-x1*", 3), + ) + .foreach { case (dataStream, expectedHits) => + val response = adminSearchManager.searchAll(dataStream) + response.totalHits should be(expectedHits) + } + } + "allow to search by data stream index name" excludeES(allEs6x, allEs7xBelowEs79x) in { + createDataStream(adminDataStream, IndexTemplateNameGenerator.next) + createDocsInDataStream(adminDataStream, 1) + adminIndexManager.rollover(adminDataStream).force() + createDocsInDataStream(adminDataStream, 1) + adminIndexManager.rollover(adminDataStream).force() + createDocsInDataStream(adminDataStream, 1) + + val allDataStreamsResponse = adminDataStreamManager.getAllDataStreams().force() + val indicesNames = allDataStreamsResponse.allBackingIndices + indicesNames.foreach { indexName => + val response = adminSearchManager.searchAll(indexName) + response.totalHits should be(1) + } + } + "allow to search by data stream index with wildcard" excludeES(allEs6x, allEs7xBelowEs79x) in { + createDataStream(adminDataStream, IndexTemplateNameGenerator.next) + createDocsInDataStream(adminDataStream, 1) + adminIndexManager.rollover(adminDataStream) + createDocsInDataStream(adminDataStream, 1) + adminIndexManager.rollover(adminDataStream) + createDocsInDataStream(adminDataStream, 1) + + List( + (".ds-data-stream*", 3), + (s".ds-$adminDataStream*", 3), + ) + .foreach { case (dataStream, expectedHits) => + val response = adminSearchManager.searchAll(dataStream) + response.totalHits should be(expectedHits) + } + } + } + "with indices rule should" - { + "allow to search by data stream index with wildcard" excludeES(allEs6x, allEs7xBelowEs79x) in { + List( + (adminDataStream, 4), + (devDataStream, 2), + (testDataStream, 1) + ) + .foreach { case (dataStream, docsCount) => + createDataStream(dataStream, IndexTemplateNameGenerator.next) + createDocsInDataStream(dataStream, docsCount) + } + + val searchManager = new SearchManager(user3Client) + + List( + (s".ds-$devDataStream*", 2), + (s".ds-$testDataStream*", 1), + (s".ds-$adminDataStream*", 0), + ) + .foreach { case (dataStream, expectedHits) => + val response = searchManager.searchAll(dataStream) + response.totalHits should be(expectedHits) + } + } + "allow to search by data stream index" excludeES(allEs6x, allEs7xBelowEs79x) in { + List( + (adminDataStream, 4), + (devDataStream, 2), + (testDataStream, 1) + ) + .foreach { case (dataStream, docsCount) => + createDataStream(dataStream, IndexTemplateNameGenerator.next) + createDocsInDataStream(dataStream, docsCount) + } + + val searchManager = new SearchManager(user3Client) + val getAllResponse = adminDataStreamManager.getAllDataStreams().force() + + def findIndicesForDataStream(name: String) = + getAllResponse.backingIndicesByDataStream(name) + + findIndicesForDataStream(adminDataStream).foreach { indexName => + val response = searchManager.searchAll(indexName) + response.responseCode should be(401) + } + findIndicesForDataStream(devDataStream).foreach { indexName => + val response = searchManager.searchAll(indexName) + response.totalHits should be(2) + } + findIndicesForDataStream(testDataStream).foreach { indexName => + val response = searchManager.searchAll(indexName) + response.totalHits should be(1) + } + } + } + "with data_streams rule should" ignore { // todo enable when search request will be aware of data streams in ACL + "allow to search by data stream name" excludeES(allEs6x, allEs7xBelowEs79x) in { + List( + (adminDataStream, 4), + (devDataStream, 2), + (testDataStream, 1) + ) + .foreach { case (dataStream, docsCount) => + createDataStream(dataStream, IndexTemplateNameGenerator.next) + createDocsInDataStream(dataStream, docsCount) + } + + val searchManager1 = new SearchManager(user1Client) + + List(devDataStream, adminDataStream).foreach { dataStream => + val response = searchManager1.searchAll(dataStream) + response.responseCode should be(401) + } + + val sm1TestDataStreamResponse = searchManager1.searchAll(testDataStream) + sm1TestDataStreamResponse.totalHits should be(1) + + val searchManager2 = new SearchManager(user6Client) + val sm2Response1 = searchManager2.searchAll(devDataStream) + sm2Response1.totalHits should be(2) + val sm2Response2 = searchManager2.searchAll(testDataStream) + sm2Response2.totalHits should be(1) + val sm2Response3 = searchManager2.searchAll(adminDataStream) + sm2Response3.responseCode should be(401) + } + "allow to search by data stream name with wildcard" excludeES(allEs6x, allEs7xBelowEs79x) in { + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(adminDataStream)).force() + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(testDataStream)).force() + + List( + s"$adminDataStream-x0", + s"$testDataStream-x1", + s"$testDataStream-x2", + s"$adminDataStream-x10", + s"$testDataStream-x11", + ).foreach { dataStream => + adminDataStreamManager.createDataStream(dataStream).force() + createDocsInDataStream(dataStream, 1) + } + + val searchManager = new SearchManager(user1Client) + + val response1 = searchManager.searchAll("data-stream*") + response1.totalHits should be(3) + val response2 = searchManager.searchAll(s"$adminDataStream*") + response2.responseCode should be(401) + val response3 = searchManager.searchAll(s"$testDataStream-x1*") + response3.totalHits should be(2) + } + } + } + "create data stream" - { + "without data_streams rule should" - { + "allow to create data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = adminDataStream + createDataStream(dataStream, IndexTemplateNameGenerator.next) + } + } + "with data_streams rule should" - { + "allow to create data stream when" - { + "the data stream name does match the allowed data stream names when" - { + "exact data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = "data-stream-prod" + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() + + val dsm = new DataStreamManager(user2Client) + val response = dsm.createDataStream(dataStream) + response.responseCode should be(200) + } + "wildcard data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("test") + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() + + val dsm = new DataStreamManager(user1Client) + val response = dsm.createDataStream(dataStream) + response.responseCode should be(200) + } + } + } + "forbid to create data stream when" - { + "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("admin") + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() + + val dsm = new DataStreamManager(user2Client) + val response = dsm.createDataStream(dataStream) + response.responseCode should be(401) + } + } + } + } + "allow to add documents to data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + createDocsInDataStream(dataStream, 1) + } + "get data stream" - { + "get all data streams" - { + "without data_streams rule should" - { + "allow to get all data streams" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("admin") + val dataStream2 = DataStreamNameGenerator.next("dev") + + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + + val response = adminDataStreamManager.getAllDataStreams() + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set(dataStream1, dataStream2)) + } + } + "with data_streams rule should" - { + "allow to get only allowed data streams when" - { + "exact data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = "data-stream-prod" + createDataStream(dataStream, IndexTemplateNameGenerator.next) + createDataStream(DataStreamNameGenerator.next("dev"), IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user2Client) + val response = dsm.getAllDataStreams() + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set(dataStream)) + } + "wildcard data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("test") + val dataStream2 = DataStreamNameGenerator.next("test") + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + createDataStream("data-stream-prod", IndexTemplateNameGenerator.next) + createDataStream(DataStreamNameGenerator.next("dev"), IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user1Client) + val response = dsm.getAllDataStreams() + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set(dataStream1, dataStream2)) + } + } + } + "with indices rule should" - { + "allow to get all data streams when" - { + "the user has access to all indices" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("admin") + val dataStream2 = DataStreamNameGenerator.next("dev") + + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user4Client) + val response = dsm.getAllDataStreams() + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set(dataStream1, dataStream2)) + } + "user has access to all backing indices" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("admin") + val dataStream2 = DataStreamNameGenerator.next("dev") + + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user4Client) + val response = dsm.getAllDataStreams() + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set(dataStream1, dataStream2)) + } + "user has access to certain backing indices" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("dev") + val dataStream2 = DataStreamNameGenerator.next("test") + val dataStream3 = DataStreamNameGenerator.next("admin") + + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + createDataStream(dataStream3, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user3Client) + val response = dsm.getAllDataStreams() + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set(dataStream1, dataStream2)) + } + } + "return empty list of data streams when" - { + "the user has no access to any backing indices" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user1Client) + val response = dsm.getAllDataStreams() + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set.empty) + } + } + } + } + "get single data stream" - { + "without data_streams rule should" - { + "allow to get data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("admin") + val dataStream2 = DataStreamNameGenerator.next("dev") + + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + + val response = adminDataStreamManager.getDataStream(dataStream1) + response.responseCode should be(200) + response.dataStreamName should be(dataStream1) + } + } + "with data_streams rule should" - { + "exact data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = "data-stream-prod" + createDataStream(dataStream, IndexTemplateNameGenerator.next) + createDataStream(DataStreamNameGenerator.next("dev"), IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user2Client) + val response = dsm.getDataStream(dataStream) + response.responseCode should be(200) + response.dataStreamName should be(dataStream) + } + "wildcard data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("test") + val dataStream2 = DataStreamNameGenerator.next("test") + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + createDataStream("data-stream-prod", IndexTemplateNameGenerator.next) + createDataStream(DataStreamNameGenerator.next("dev"), IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user1Client) + val response = dsm.getDataStream(dataStream1) + response.responseCode should be(200) + response.dataStreamName should be(dataStream1) + } + } + "with indices rule should" - { + "allow to get data stream when" - { + "the index name does match the allowed indices" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("test") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user3Client) + val response = dsm.getDataStream(dataStream) + response.responseCode should be(200) + response.dataStreamName should be(dataStream) + } + } + "return empty data streams when" - { + "the index name does not match the allowed indices" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user3Client) + val response = dsm.getDataStream(dataStream) + response.responseCode should be(200) + response.allDataStreams.toSet should be(Set.empty) + } + } + } + } + } + "get data stream stats" - { + "without data_streams rule should" - { + "allow to get data stream stats" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("admin") + val dataStream2 = DataStreamNameGenerator.next("admin") + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDocsInDataStream(dataStream1, 1) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + createDocsInDataStream(dataStream2, 2) + val response = adminDataStreamManager.getDataStreamStats("*") + response.responseCode should be(200) + response.dataStreamsCount should be(2) + response.backingIndicesCount should be(2) + } + } + "with data_streams rule should" - { + "allow to get data stream stats when" - { + "the data stream name does match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("test") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user1Client) + val response = dsm.getDataStreamStats(dataStream) + response.responseCode should be(200) + response.responseCode should be(200) + response.dataStreamsCount should be(1) + response.backingIndicesCount should be(1) + } + } + "forbid to get data stream stats when" - { + "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user1Client) + val response = dsm.getDataStreamStats(dataStream) + response.responseCode should be(401) + } + } + } + } + "delete data stream" - { + "without data_streams rule should" - { + "allow to delete all data streams" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("admin") + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + val response = adminDataStreamManager.deleteDataStream(dataStream1) + response.responseCode should be(200) + } + } + "with data_streams rule should" - { + "allow to delete only allowed data streams when" - { + "exact data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = "data-stream-prod" + createDataStream(dataStream, IndexTemplateNameGenerator.next) + createDataStream(DataStreamNameGenerator.next("dev"), IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user2Client) + val response = dsm.deleteDataStream(dataStream) + response.responseCode should be(200) + } + "wildcard data stream matching" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream1 = DataStreamNameGenerator.next("test") + val dataStream2 = DataStreamNameGenerator.next("test") + createDataStream(dataStream1, IndexTemplateNameGenerator.next) + createDataStream(dataStream2, IndexTemplateNameGenerator.next) + createDataStream("data-stream-prod", IndexTemplateNameGenerator.next) + createDataStream(DataStreamNameGenerator.next("dev"), IndexTemplateNameGenerator.next) + + val dsm = new DataStreamManager(user1Client) + val response = dsm.deleteDataStream(dataStream1) + response.responseCode should be(200) + } + } + "forbid to delete data stream when" - { + "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("admin") + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() + + val dsm = new DataStreamManager(user2Client) + val response = dsm.deleteDataStream(dataStream) + response.responseCode should be(401) + } + } + } + } + "should allow to rollover data stream" excludeES(allEs6x, allEs7xBelowEs79x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + List.range(0, 2).foreach { _ => + createDocsInDataStream(dataStream, 1) + adminIndexManager.rollover(dataStream).force() + } + + val statsResponse = adminDataStreamManager.getDataStreamStats(dataStream) + statsResponse.responseCode should be(200) + statsResponse.dataStreamsCount should be(1) + statsResponse.backingIndicesCount should be(3) + } + "migrate index alias to data stream" - { + "without indices rule should" - { + "allow to migrate index alias to data stream" excludeES(allEs6x, allEs7xBelowEs711x) in { + val dataStream = DataStreamNameGenerator.next("admin") + adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test1", "@timestamp": "${format(Instant.now())}"}""")).force() + adminDocumentManager.createDoc("logs-0001", 2, ujson.read(s"""{ "message":"test2", "@timestamp": "${format(Instant.now())}"}""")).force() + adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test3", "@timestamp": "${format(Instant.now())}"}""")).force() + adminIndexManager.createAliasOf("logs-0001", dataStream).force() + + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() + + val migrateToDataStreamResponse = adminDataStreamManager.migrateToDataStream(dataStream) + migrateToDataStreamResponse.responseCode should be(200) + + val statsResponse = adminDataStreamManager.getDataStreamStats(dataStream) + statsResponse.responseCode should be(200) + statsResponse.dataStreamsCount should be(1) + statsResponse.backingIndicesCount should be(1) + } + } + "with indices rule should" - { + "allow to migrate index alias to data stream when" - { + "the alias does match the allowed indices" excludeES(allEs6x, allEs7xBelowEs711x) in { + val dataStream = DataStreamNameGenerator.next("test") + adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test1", "@timestamp": "${format(Instant.now())}"}""")).force() + adminDocumentManager.createDoc("logs-0001", 2, ujson.read(s"""{ "message":"test2", "@timestamp": "${format(Instant.now())}"}""")).force() + adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test3", "@timestamp": "${format(Instant.now())}"}""")).force() + adminIndexManager.createAliasOf("logs-0001", dataStream).force() + + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() + + val dsm = new DataStreamManager(user5Client) + val migrateToDataStreamResponse = dsm.migrateToDataStream(dataStream) + migrateToDataStreamResponse.responseCode should be(200) + + val statsResponse = adminDataStreamManager.getDataStreamStats(dataStream) + statsResponse.responseCode should be(200) + statsResponse.dataStreamsCount should be(1) + statsResponse.backingIndicesCount should be(1) + } + } + "forbid to migrate index alias to data stream when" - { + "the alias does not match the allowed indices" excludeES(allEs6x, allEs7xBelowEs711x) in { + val dataStream = DataStreamNameGenerator.next("admin") + adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test1", "@timestamp": "${format(Instant.now())}"}""")).force() + adminDocumentManager.createDoc("logs-0001", 2, ujson.read(s"""{ "message":"test2", "@timestamp": "${format(Instant.now())}"}""")).force() + adminDocumentManager.createDoc("logs-0001", 1, ujson.read(s"""{ "message":"test3", "@timestamp": "${format(Instant.now())}"}""")).force() + adminIndexManager.createAliasOf("logs-0001", dataStream).force() + + adminTemplateManager.createTemplate(IndexTemplateNameGenerator.next, indexTemplate(dataStream)).force() + + val dsm = new DataStreamManager(user5Client) + val migrateToDataStreamResponse = dsm.migrateToDataStream(dataStream) + migrateToDataStreamResponse.responseCode should be(401) + } + } + } + } + "modify data stream" - { + "without data_streams rule should" - { + "allow to modify data streams" excludeES(allEs6x, allEs7xBelowEs716x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + List.range(0, 2).foreach { _ => + createDocsInDataStream(dataStream, 1) + adminIndexManager.rollover(dataStream).force() + } + + val dsIndices = dataStreamBackingIndices(dataStream) + dsIndices.length should be(3) + + val modifyResponse = adminDataStreamManager.modifyDataStreams(ujson.read( + s""" + |{ + | "actions": [ + | { + | "remove_backing_index": { + | "data_stream": "$dataStream", + | "index": "${dsIndices.head}" + | } + | } + | ] + |} + |""".stripMargin)) + modifyResponse.responseCode should be(200) + } + } + "with data_streams rule should" - { + "allow to modify only allowed data streams when" - { + "exact data stream matching" excludeES(allEs6x, allEs7xBelowEs716x) in { + val dataStream = "data-stream-prod" + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + List.range(0, 2).foreach { _ => + createDocsInDataStream(dataStream, 1) + adminIndexManager.rollover(dataStream).force() + } + val dsIndices = dataStreamBackingIndices(dataStream) + dsIndices.length should be(3) + + val dsm = new DataStreamManager(user2Client) + val modifyResponse = dsm.modifyDataStreams(ujson.read( + s""" + |{ + | "actions": [ + | { + | "remove_backing_index": { + | "data_stream": "$dataStream", + | "index": "${dsIndices.head}" + | } + | } + | ] + |} + |""".stripMargin)) + modifyResponse.responseCode should be(200) + } + "wildcard data stream matching" excludeES(allEs6x, allEs7xBelowEs716x) in { + val dataStream = DataStreamNameGenerator.next("test") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + List.range(0, 2).foreach { _ => + createDocsInDataStream(dataStream, 1) + adminIndexManager.rollover(dataStream).force() + } + + val dsIndices = dataStreamBackingIndices(dataStream) + dsIndices.length should be(3) + + val dsm = new DataStreamManager(user1Client) + val modifyResponse = dsm.modifyDataStreams(ujson.read( + s""" + |{ + | "actions": [ + | { + | "remove_backing_index": { + | "data_stream": "$dataStream", + | "index": "${dsIndices.head}" + | } + | } + | ] + |} + |""".stripMargin)) + modifyResponse.responseCode should be(200) + } + } + "forbid to modify data streams when" - { + "the data stream name does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs716x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + List.range(0, 2).foreach { _ => + createDocsInDataStream(dataStream, 1) + adminIndexManager.rollover(dataStream).force() + } + val dsIndices = dataStreamBackingIndices(dataStream) + dsIndices.length should be(3) + + + val dsm = new DataStreamManager(user1Client) + val modifyResponse = dsm.modifyDataStreams(ujson.read( + s""" + |{ + | "actions": [ + | { + | "remove_backing_index": { + | "data_stream": "$dataStream", + | "index": "${dsIndices.head}" + | } + | } + | ] + |} + |""".stripMargin)) + modifyResponse.responseCode should be(401) + } + "one of the data streams does not match the allowed data stream names" excludeES(allEs6x, allEs7xBelowEs716x) in { + val dataStream = DataStreamNameGenerator.next("test") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + val dsIndices = dataStreamBackingIndices(dataStream) + dsIndices.length should be(1) + val forbiddenDataStream = DataStreamNameGenerator.next("admin") + createDataStream(forbiddenDataStream, IndexTemplateNameGenerator.next) + val forbiddenDsIndices = dataStreamBackingIndices(forbiddenDataStream) + forbiddenDsIndices.length should be(1) + + val dsm = new DataStreamManager(user1Client) + val modifyResponse = dsm.modifyDataStreams(ujson.read( + s""" + |{ + | "actions": [ + | { + | "remove_backing_index": { + | "data_stream": "$dataStream", + | "index": "${dsIndices.head}" + | } + | }, + | { + | "remove_backing_index": { + | "data_stream": "$forbiddenDataStream", + | "index": "${forbiddenDsIndices.head}" + | } + | } + | ] + |} + |""".stripMargin)) + modifyResponse.responseCode should be(401) + } + } + } + "with indices rule should" - { + "allow to modify only data streams when" - { + "backing index name matches allowed indices" excludeES(allEs6x, allEs7xBelowEs716x) in { + val dataStream = DataStreamNameGenerator.next("test") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + List.range(0, 2).foreach { _ => + createDocsInDataStream(dataStream, 1) + adminIndexManager.rollover(dataStream).force() + } + val dsIndices = dataStreamBackingIndices(dataStream) + dsIndices.length should be(3) + + val dsm = new DataStreamManager(user3Client) + val modifyResponse = dsm.modifyDataStreams(ujson.read( + s""" + |{ + | "actions": [ + | { + | "remove_backing_index": { + | "data_stream": "$dataStream", + | "index": "${dsIndices.head}" + | } + | } + | ] + |} + |""".stripMargin)) + modifyResponse.responseCode should be(200) + } + } + "forbid to modify data stream when" - { + "backing index name does not match allowed indices" excludeES(allEs6x, allEs7xBelowEs716x) in { + val dataStream = DataStreamNameGenerator.next("admin") + createDataStream(dataStream, IndexTemplateNameGenerator.next) + + List.range(0, 2).foreach { _ => + createDocsInDataStream(dataStream, 1) + adminIndexManager.rollover(dataStream).force() + } + val dsIndices = dataStreamBackingIndices(dataStream) + dsIndices.length should be(3) + + val dsm = new DataStreamManager(user3Client) + val modifyResponse = dsm.modifyDataStreams(ujson.read( + s""" + |{ + | "actions": [ + | { + | "remove_backing_index": { + | "data_stream": "$dataStream", + | "index": "${dsIndices.head}" + | } + | } + | ] + |} + |""".stripMargin)) + modifyResponse.responseCode should be(401) + } + } + } + } + } + + private def createDataStream(dataStreamName: String, indexTemplateName: String): Unit = { + adminTemplateManager.createTemplate(indexTemplateName, indexTemplate(dataStreamName)).force() + val createDataStreamResponse = adminDataStreamManager.createDataStream(dataStreamName) + createDataStreamResponse.responseCode should be(200) + } + + private def createDocsInDataStream(streamName: String, count: Int): Unit = { + List.range(0, count).foreach { c => + val doc = ujson.read(s"""{ "message":"test$c", "@timestamp": "${format(Instant.now())}"}""") + adminDocumentManager.createDocWithGeneratedId(streamName, doc).force() + } + } + + private def dataStreamBackingIndices(streamName: String): List[String] = { + adminDataStreamManager + .getDataStream(streamName) + .force() + .backingIndices + } + + private def indexTemplate(dataStreamName: String) = ujson.read( + s""" + |{ + | "index_patterns": ["$dataStreamName*"], + | "data_stream": { }, + | "priority": 500, + | "template": { + | "mappings": { + | "properties": { + | "@timestamp": { + | "type": "date", + | "format": "date_optional_time||epoch_millis" + | }, + | "message": { + | "type": "wildcard" + | } + | } + | } + | } + |} + |""".stripMargin + ) + + private def format(instant: Instant) = instant.toString + + override def beforeEach(): Unit = { + if (Version.greaterOrEqualThan(esVersionUsed, 7, 9, 0)) { + val dataStreamsResponse = adminDataStreamManager.getAllDataStreams().force() + dataStreamsResponse.allDataStreams.foreach { dataStream => + adminDataStreamManager.deleteDataStream(dataStream).force() + } + + adminTemplateManager + .getTemplates + .templates + .filter(_.name.startsWith("index-template")) + .foreach { template => + adminTemplateManager.deleteTemplate(template.name).force() + } + } + super.beforeEach() + } +} + +private object DataStreamApiSuite { + object IndexTemplateNameGenerator { + private val uniquePart = Atomic(0) + + def next: String = s"index-template-${uniquePart.incrementAndGet()}" + } + + object DataStreamNameGenerator { + def next(infix: String): String = s"data-stream-$infix-$randomSuffix" + + private def randomSuffix: String = Random.alphanumeric.take(12).mkString.toLowerCase + } +} diff --git a/tests-utils/src/main/scala/tech/beshu/ror/utils/elasticsearch/DataStreamManager.scala b/tests-utils/src/main/scala/tech/beshu/ror/utils/elasticsearch/DataStreamManager.scala index 45599af541..ad23813d43 100644 --- a/tests-utils/src/main/scala/tech/beshu/ror/utils/elasticsearch/DataStreamManager.scala +++ b/tests-utils/src/main/scala/tech/beshu/ror/utils/elasticsearch/DataStreamManager.scala @@ -16,9 +16,11 @@ */ package tech.beshu.ror.utils.elasticsearch +import org.apache.http.HttpResponse import org.apache.http.client.methods.{HttpDelete, HttpGet, HttpPost, HttpPut} import org.apache.http.entity.StringEntity import tech.beshu.ror.utils.elasticsearch.BaseManager.{JSON, JsonResponse} +import tech.beshu.ror.utils.elasticsearch.DataStreamManager._ import tech.beshu.ror.utils.httpclient.RestClient class DataStreamManager(client: RestClient) extends BaseManager(client) { @@ -28,19 +30,19 @@ class DataStreamManager(client: RestClient) extends BaseManager(client) { call(request, new JsonResponse(_)) } - def getAllDataStreams(): JsonResponse = { + def getAllDataStreams(): GetAllDataStreamsResult = { val request = new HttpGet(client.from(s"/_data_stream/")) - call(request, new JsonResponse(_)) + call(request, new GetAllDataStreamsResult(_)) } - def getDataStream(name: String): JsonResponse = { + def getDataStream(name: String): GetDataStreamResult = { val request = new HttpGet(client.from(s"/_data_stream/$name")) - call(request, new JsonResponse(_)) + call(request, new GetDataStreamResult(_)) } - def getDataStreamStats(name: String): JsonResponse = { + def getDataStreamStats(name: String): DataStreamStatsResult = { val request = new HttpGet(client.from(s"/_data_stream/$name/_stats")) - call(request, new JsonResponse(_)) + call(request, new DataStreamStatsResult(_)) } def deleteDataStream(name: String): JsonResponse = { @@ -60,3 +62,46 @@ class DataStreamManager(client: RestClient) extends BaseManager(client) { call(request, new JsonResponse(_)) } } + +object DataStreamManager { + + class GetAllDataStreamsResult(response: HttpResponse) extends JsonResponse(response) { + + def allBackingIndices: List[String] = { + dataStreamWithBackingIndices.values.flatten.toList + } + + def allDataStreams: List[String] = { + dataStreamWithBackingIndices.keys.toList + } + + def backingIndicesByDataStream(dataStream: String): List[String] = { + dataStreamWithBackingIndices(dataStream) + } + + private lazy val dataStreamWithBackingIndices: Map[String, List[String]] = + responseJson("data_streams").arr + .map { dataStream => + ( + dataStream("name").str, + dataStream("indices").arr.map(_("index_name").str).toList + ) + } + .toMap + } + + + class GetDataStreamResult(response: HttpResponse) extends GetAllDataStreamsResult(response) { + + def dataStreamName: String = allDataStreams.head + + def backingIndices: List[String] = allBackingIndices + } + + class DataStreamStatsResult(response: HttpResponse) extends JsonResponse(response) { + + def dataStreamsCount: Int = responseJson("data_stream_count").num.toInt + + def backingIndicesCount: Int = responseJson("backing_indices").num.toInt + } +}