diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1cf78e045f..e4c776c872 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -108,6 +108,8 @@ stages: strategy: maxParallel: 99 matrix: + IT_es813x: + ROR_TASK: integration_es813x IT_es812x: ROR_TASK: integration_es812x IT_es811x: @@ -160,8 +162,8 @@ stages: strategy: maxParallel: 99 matrix: - IT_es812x: - ROR_TASK: integration_es812x + IT_es813x: + ROR_TASK: integration_es813x IT_es80x: ROR_TASK: integration_es80x - job: diff --git a/ci/run-pipeline.sh b/ci/run-pipeline.sh index f5c9301f20..7d8950a12e 100755 --- a/ci/run-pipeline.sh +++ b/ci/run-pipeline.sh @@ -50,6 +50,10 @@ run_integration_tests() { ./gradlew integration-tests:test "-PesModule=$ES_MODULE" || (find . | grep hs_err | xargs cat && exit 1) } +if [[ -z $TRAVIS ]] || [[ $ROR_TASK == "integration_es813x" ]]; then + run_integration_tests "es813x" +fi + if [[ -z $TRAVIS ]] || [[ $ROR_TASK == "integration_es812x" ]]; then run_integration_tests "es812x" fi diff --git a/ci/supported-es-versions/es7x.txt b/ci/supported-es-versions/es7x.txt index 3777aa55d2..3a264466ca 100644 --- a/ci/supported-es-versions/es7x.txt +++ b/ci/supported-es-versions/es7x.txt @@ -1,3 +1,4 @@ +7.17.19 7.17.18 7.17.17 7.17.16 diff --git a/ci/supported-es-versions/es8x.txt b/ci/supported-es-versions/es8x.txt index ffa6be678a..5739069bf3 100644 --- a/ci/supported-es-versions/es8x.txt +++ b/ci/supported-es-versions/es8x.txt @@ -1,3 +1,4 @@ +8.13.0 8.12.2 8.12.1 8.12.0 diff --git a/es717x/gradle.properties b/es717x/gradle.properties index 17da27357f..58e40ba583 100644 --- a/es717x/gradle.properties +++ b/es717x/gradle.properties @@ -1 +1 @@ -latestSupportedEsVersion=7.17.18 +latestSupportedEsVersion=7.17.19 diff --git a/es813x/Dockerfile b/es813x/Dockerfile new file mode 100644 index 0000000000..4330589294 --- /dev/null +++ b/es813x/Dockerfile @@ -0,0 +1,29 @@ +ARG ES_VERSION +ARG ROR_VERSION + +FROM docker.elastic.co/elasticsearch/elasticsearch:${ES_VERSION} + +ARG ES_VERSION +ARG ROR_VERSION + +ENV KIBANA_USER_PASS=kibana +ENV ADMIN_USER_PASS=admin + +USER root + +RUN apt-get update && \ + apt-get install -y --no-install-recommends gosu && \ + rm -rf /var/lib/apt/lists/* && \ + gosu nobody true + +USER elasticsearch + +COPY readonlyrest-${ROR_VERSION}_es${ES_VERSION}.zip /tmp/readonlyrest.zip +COPY init-readonlyrest.yml /usr/share/elasticsearch/config/readonlyrest.yml +COPY ror-entrypoint.sh /usr/local/bin/ror-entrypoint.sh + +RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch file:///tmp/readonlyrest.zip + +USER root + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/ror-entrypoint.sh"] \ No newline at end of file diff --git a/es813x/build.gradle b/es813x/build.gradle new file mode 100644 index 0000000000..fb12ee02b1 --- /dev/null +++ b/es813x/build.gradle @@ -0,0 +1,165 @@ +/* + * 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/ + */ +import com.bmuschko.gradle.docker.tasks.image.* +import org.gradle.crypto.checksum.Checksum + +plugins { + id 'com.bmuschko.docker-remote-api' + id 'org.gradle.crypto.checksum' + id "readonlyrest.plugin-common-conventions" +} + +def pluginFullName = pluginName + '-' + version +def projectJavaLanguageVersion = JavaLanguageVersion.of(17) +def moduleEsVersion = project.property('esVersion') != null ? project.property('esVersion') : project.property('latestSupportedEsVersion') +def dockerImageLatest = 'beshultd/elasticsearch-readonlyrest:' + moduleEsVersion + '-ror-latest' +def dockerImageVersion = 'beshultd/elasticsearch-readonlyrest:' + moduleEsVersion + '-ror-' + pluginVersion + +java { + toolchain { + languageVersion = projectJavaLanguageVersion + } +} + +dependencies { + implementation project(path: ':core') + implementation project(path: ':ror-tools', configuration: 'shadow') + implementation project(path: ':ror-tools-core') + implementation group: 'org.elasticsearch', name: 'elasticsearch' , version: moduleEsVersion + implementation group: 'org.elasticsearch.client', name: 'elasticsearch-rest-client', version: moduleEsVersion + implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1' + compileOnly group: 'org.locationtech.spatial4j', name: 'spatial4j', version: '0.7' + // if you don't have this dependency in local maven, please run publishToMavenLocal task first + compileOnly group: 'org.elasticsearch.plugin', name: 'transport-netty4', version: moduleEsVersion +} + +configurations { + wagon + distJars { + exclude group: 'org.elasticsearch' + exclude group: 'lucene-core' + exclude module: 'log4j-api' + exclude module: 'log4j-core' + exclude group: 'lucene-analyzers-common' + exclude group: 'org.apache.commons' + exclude group: 'org.yaml' + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' + } +} + +tasks.register('cleanOldData') { + doLast { + delete 'build/tmp/' + pluginFullName + } +} + +tasks.register('jarHellCheck', JavaExec) { + outputs.upToDateWhen { false } + mainClass.set("org.elasticsearch.jdk.JarHell") + classpath = project.sourceSets.main.compileClasspath.filter { it.exists() } + javaLauncher = javaToolchains.launcherFor { + languageVersion = projectJavaLanguageVersion + } +} + +tasks.register('resolvePluginDescriptorTemplate', Copy) { + outputs.upToDateWhen { false } + from './plugin-metadata' + into 'build/tmp/' + pluginFullName + expand([ + 'descriptor': [ + 'name' : pluginName, + 'pluginVersion': pluginVersion, + 'esVersion' : moduleEsVersion + ] + ]) +} + +tasks.register('buildRorPluginZip') { + dependsOn(packageRorPlugin, createRorPluginChecksums) +} + +tasks.register('packageRorPlugin', Zip) { + dependsOn(cleanOldData, jarHellCheck, toJar, resolvePluginDescriptorTemplate) + outputs.upToDateWhen { false } + archivesBaseName = pluginName + into('.') { + from configurations.distJars.filter { x -> !x.name.contains('spatial4j') && !x.name.contains('jts') } + from 'build/libs/' + pluginFullName + '.jar' + from 'build/tmp/' + pluginFullName + } +} + +tasks.register('createRorPluginChecksums', Checksum) { + dependsOn(packageRorPlugin) + def distributionsDir = layout.buildDirectory.dir("distributions") + + outputs.upToDateWhen { false } + inputFiles.setFrom(packageRorPlugin.archiveFile) + outputDirectory.set(distributionsDir) + checksumAlgorithm.set(Checksum.Algorithm.SHA512) +} + +tasks.register('uploadRorPluginToS3', Exec) { + dependsOn(packageRorPlugin, createRorPluginChecksums) + + def distributionsDir = layout.buildDirectory.get().asFile.path + "/distributions" + def pluginFileZip = distributionsDir + "/" + pluginFullName + ".zip" + def pluginSha1 = distributionsDir + "/" + pluginFullName + ".zip.sha1" + def pluginSha512 = distributionsDir + "/" + pluginFullName + ".zip.sha512" + def targetS3Dir = "build/" + pluginVersion + "/" + + commandLine '../ci/upload-files-to-s3.sh', pluginFileZip, pluginSha512, pluginSha1, targetS3Dir +} + +tasks.register('prepareDockerImageFiles', Copy) { + dependsOn packageRorPlugin + outputs.upToDateWhen { false } + + from layout.projectDirectory.file("Dockerfile") + from layout.buildDirectory.file("distributions/" + pluginFullName + ".zip") + from rootProject.files("docker-image") + + into layout.buildDirectory.dir("docker-image") +} + +tasks.register('buildRorDockerImage', DockerBuildImage) { + dependsOn('packageRorPlugin', 'prepareDockerImageFiles') + + inputDir = layout.buildDirectory.dir("docker-image") + buildArgs = [ + ES_VERSION: project.properties['esVersion'], + ROR_VERSION: pluginVersion + ] + images.add(dockerImageLatest) + images.add(dockerImageVersion) +} + +tasks.register('removeRorDockerImage', DockerRemoveImage) { + dependsOn buildRorDockerImage + force = true + targetImageId buildRorDockerImage.getImageId() +} + +tasks.register('pushRorDockerImage', DockerPushImage) { + dependsOn buildRorDockerImage + + images.add(dockerImageLatest) + images.add(dockerImageVersion) +} +pushRorDockerImage.configure { finalizedBy removeRorDockerImage } \ No newline at end of file diff --git a/es813x/gradle.properties b/es813x/gradle.properties new file mode 100644 index 0000000000..d450040a20 --- /dev/null +++ b/es813x/gradle.properties @@ -0,0 +1 @@ +latestSupportedEsVersion=8.13.0 \ No newline at end of file diff --git a/es813x/plugin-metadata/plugin-descriptor.properties b/es813x/plugin-metadata/plugin-descriptor.properties new file mode 100644 index 0000000000..a383212961 --- /dev/null +++ b/es813x/plugin-metadata/plugin-descriptor.properties @@ -0,0 +1,27 @@ +# +# 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/ +# +# +# Elasticsearch plugin descriptor file +# This file must exist as 'plugin-descriptor.properties' in a folder named `elasticsearch` +# inside all plugins. +# +name=${descriptor.name} +version=${descriptor.pluginVersion} +description=Safely expose Elasticsearch REST API +classname=tech.beshu.ror.es.ReadonlyRestPlugin +java.version=1.8 +elasticsearch.version=${descriptor.esVersion} diff --git a/es813x/plugin-metadata/plugin-security.policy b/es813x/plugin-metadata/plugin-security.policy new file mode 100644 index 0000000000..a201fd0b71 --- /dev/null +++ b/es813x/plugin-metadata/plugin-security.policy @@ -0,0 +1,14 @@ +grant { + permission java.io.FilePermission "/usr/share/elasticsearch", "read"; + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + permission java.lang.RuntimePermission "accessClassInPackage.sun.misc"; + permission java.lang.RuntimePermission "accessClassInPackage.sun.misc.*"; + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "setContextClassLoader"; + permission java.net.SocketPermission "*", "accept, resolve, connect"; + permission java.security.SecurityPermission "insertProvider"; + permission java.security.SecurityPermission "putProviderProperty.BCFIPS"; + permission java.security.SecurityPermission "putProviderProperty.BCJSSE"; + permission java.util.PropertyPermission "*", "read,write"; +}; \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/IndexLevelActionFilter.scala b/es813x/src/main/scala/tech/beshu/ror/es/IndexLevelActionFilter.scala new file mode 100644 index 0000000000..76669d83b4 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/IndexLevelActionFilter.scala @@ -0,0 +1,219 @@ +/* + * 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 + +import monix.execution.atomic.Atomic +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.support.{ActionFilter, ActionFilterChain} +import org.elasticsearch.action.{ActionListener, ActionRequest, ActionResponse} +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.cluster.service.ClusterService +import org.elasticsearch.env.Environment +import org.elasticsearch.repositories.RepositoriesService +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.RemoteClusterService +import tech.beshu.ror.accesscontrol.domain.{Action, AuditCluster} +import tech.beshu.ror.accesscontrol.matchers.UniqueIdentifierGenerator +import tech.beshu.ror.boot.ReadonlyRest.AuditSinkCreator +import tech.beshu.ror.boot.RorSchedulers.Implicits.mainScheduler +import tech.beshu.ror.boot._ +import tech.beshu.ror.boot.engines.Engines +import tech.beshu.ror.configuration.EnvironmentConfig +import tech.beshu.ror.es.handler.{AclAwareRequestFilter, RorNotAvailableRequestHandler} +import tech.beshu.ror.es.handler.AclAwareRequestFilter.{EsChain, EsContext} +import tech.beshu.ror.es.handler.response.ForbiddenResponse.createTestSettingsNotConfiguredResponse +import tech.beshu.ror.es.services.{EsAuditSinkService, EsIndexJsonContentService, EsServerBasedRorClusterService, RestClientAuditSinkService} +import tech.beshu.ror.es.utils.ThreadContextOps.createThreadContextOps +import tech.beshu.ror.es.utils.ThreadRepo +import tech.beshu.ror.exceptions.StartingFailureException +import tech.beshu.ror.utils.AccessControllerHelper._ +import tech.beshu.ror.utils.{JavaConverters, RorInstanceSupplier} + +import java.util.function.Supplier + +class IndexLevelActionFilter(nodeName: String, + clusterService: ClusterService, + client: NodeClient, + threadPool: ThreadPool, + env: Environment, + remoteClusterServiceSupplier: Supplier[Option[RemoteClusterService]], + repositoriesServiceSupplier: Supplier[Option[RepositoriesService]], + esInitListener: EsInitListener, + rorEsConfig: ReadonlyRestEsConfig) + (implicit environmentConfig: EnvironmentConfig) + extends ActionFilter with Logging { + + private implicit val generator: UniqueIdentifierGenerator = environmentConfig.uniqueIdentifierGenerator + + private val rorNotAvailableRequestHandler: RorNotAvailableRequestHandler = + new RorNotAvailableRequestHandler(rorEsConfig.bootConfig) + + private val ror = ReadonlyRest.create( + new EsIndexJsonContentService(client), + auditSinkCreator, + env.configFile + ) + + private val rorInstanceState: Atomic[RorInstanceStartingState] = + Atomic(RorInstanceStartingState.Starting: RorInstanceStartingState) + + private val aclAwareRequestFilter = new AclAwareRequestFilter( + new EsServerBasedRorClusterService( + nodeName, + clusterService, + remoteClusterServiceSupplier, + repositoriesServiceSupplier, + client, + threadPool + ), + clusterService.getSettings, + threadPool + ) + + private val startingTaskCancellable = startRorInstance() + + private def auditSinkCreator: AuditSinkCreator = { + case AuditCluster.LocalAuditCluster => + new EsAuditSinkService(client) + case remote: AuditCluster.RemoteAuditCluster => + RestClientAuditSinkService.create(remote) + } + + override def order(): Int = 0 + + def stop(): Unit = { + startingTaskCancellable.cancel() + rorInstanceState.get() match { + case RorInstanceStartingState.Starting => + case RorInstanceStartingState.Started(instance) => instance.stop().runSyncUnsafe() + case RorInstanceStartingState.NotStarted(_) => + } + } + + override def apply[Request <: ActionRequest, Response <: ActionResponse](task: Task, + action: String, + request: Request, + listener: ActionListener[Response], + chain: ActionFilterChain[Request, Response]): Unit = { + doPrivileged { + proceed( + task, + Action(action), + request, + listener.asInstanceOf[ActionListener[ActionResponse]], + new EsChain(chain.asInstanceOf[ActionFilterChain[ActionRequest, ActionResponse]]) + ) + } + } + + private def proceed(task: Task, + action: Action, + request: ActionRequest, + listener: ActionListener[ActionResponse], + chain: EsChain): Unit = { + ThreadRepo.getRorRestChannel match { + case None => + threadPool.getThreadContext.addXpackSecurityAuthenticationHeader(nodeName) + chain.continue(task, action, request, listener) + case Some(_) if action.isInternal => + threadPool.getThreadContext.addSystemAuthenticationHeader(nodeName) + chain.continue(task, action, request, listener) + case Some(channel) => + proceedByRorEngine( + EsContext( + channel, + nodeName, + task, + action, + request, + listener, + chain, + JavaConverters.flattenPair(threadPool.getThreadContext.getResponseHeaders).toSet + ) + ) + } + } + + private def proceedByRorEngine(esContext: EsContext): Unit = { + rorInstanceState.get() match { + case RorInstanceStartingState.Starting => + handleRorNotReadyYet(esContext) + case RorInstanceStartingState.Started(instance) => + instance.engines match { + case Some(engines) => + handleRequest(engines, esContext) + case None => + handleRorNotReadyYet(esContext) + } + case RorInstanceStartingState.NotStarted(_) => + handleRorFailedToStart(esContext) + } + } + + private def handleRequest(engines: Engines, esContext: EsContext): Unit = { + aclAwareRequestFilter + .handle(engines, esContext) + .runAsync { + case Right(result) => handleResult(esContext, result) + case Left(ex) => esContext.listener.onFailure(new Exception(ex)) + } + } + + private def handleResult(esContext: EsContext, result: Either[AclAwareRequestFilter.Error, Unit]): Unit = result match { + case Right(_) => + case Left(AclAwareRequestFilter.Error.ImpersonatorsEngineNotConfigured) => + esContext.listener.onFailure(createTestSettingsNotConfiguredResponse()) + } + + private def handleRorNotReadyYet(esContext: EsContext): Unit = { + logger.warn(s"[${esContext.requestContextId}] Cannot handle the request ${esContext.channel.request().path()} because ReadonlyREST hasn't started yet") + rorNotAvailableRequestHandler.handleRorNotReadyYet(esContext) + } + + private def handleRorFailedToStart(esContext: EsContext): Unit = { + logger.error(s"[${esContext.requestContextId}] Cannot handle the ${esContext.channel.request().path()} request because ReadonlyREST failed to start") + rorNotAvailableRequestHandler.handleRorFailedToStart(esContext) + } + + private def startRorInstance() = { + val startResult = for { + _ <- esInitListener.waitUntilReady + result <- ror.start() + } yield result + startResult.runAsync { + case Right(Right(instance)) => + RorInstanceSupplier.update(instance) + rorInstanceState.set(RorInstanceStartingState.Started(instance)) + case Right(Left(failure)) => + val startingFailureException = StartingFailureException.from(failure) + logger.error("ROR starting failure:", startingFailureException) + rorInstanceState.set(RorInstanceStartingState.NotStarted(startingFailureException)) + case Left(ex) => + val startingFailureException = StartingFailureException.from(ex) + logger.error("ROR starting failure:", startingFailureException) + rorInstanceState.set(RorInstanceStartingState.NotStarted(StartingFailureException.from(startingFailureException))) + } + } +} + +private sealed trait RorInstanceStartingState +private object RorInstanceStartingState { + case object Starting extends RorInstanceStartingState + final case class Started(instance: RorInstance) extends RorInstanceStartingState + final case class NotStarted(cause: StartingFailureException) extends RorInstanceStartingState +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/ReadonlyRestEsConfig.scala b/es813x/src/main/scala/tech/beshu/ror/es/ReadonlyRestEsConfig.scala new file mode 100644 index 0000000000..3691aa624a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/ReadonlyRestEsConfig.scala @@ -0,0 +1,41 @@ +/* + * 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 + +import cats.data.EitherT +import monix.eval.Task +import tech.beshu.ror.configuration.{EnvironmentConfig, FipsConfiguration, MalformedSettings, RorBootConfiguration, RorSsl} +import tech.beshu.ror.utils.ScalaOps._ + +import java.nio.file.Path + +final case class ReadonlyRestEsConfig(bootConfig: RorBootConfiguration, + sslConfig: RorSsl, + fipsConfig: FipsConfiguration) + +object ReadonlyRestEsConfig { + def load(esConfigFolderPath: Path) + (implicit environmentConfig: EnvironmentConfig): Task[Either[MalformedSettings, ReadonlyRestEsConfig]] = { + value { + for { + bootConfig <- EitherT(RorBootConfiguration.load(esConfigFolderPath)) + sslConfig <- EitherT(RorSsl.load(esConfigFolderPath)) + fipsConfig <- EitherT(FipsConfiguration.load(esConfigFolderPath)) + } yield ReadonlyRestEsConfig(bootConfig, sslConfig, fipsConfig) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/ReadonlyRestPlugin.scala b/es813x/src/main/scala/tech/beshu/ror/es/ReadonlyRestPlugin.scala new file mode 100644 index 0000000000..3eb1352a6a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/ReadonlyRestPlugin.scala @@ -0,0 +1,247 @@ +/* + * 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 + +import monix.execution.Scheduler +import monix.execution.schedulers.CanBlock +import org.elasticsearch.ElasticsearchException +import org.elasticsearch.action.support.ActionFilter +import org.elasticsearch.action.{ActionRequest, ActionResponse} +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver +import org.elasticsearch.cluster.node.DiscoveryNodes +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.common.io.stream.NamedWriteableRegistry +import org.elasticsearch.common.network.NetworkService +import org.elasticsearch.common.settings._ +import org.elasticsearch.common.util.concurrent.{EsExecutors, ThreadContext} +import org.elasticsearch.common.util.{BigArrays, PageCacheRecycler} +import org.elasticsearch.env.Environment +import org.elasticsearch.features.NodeFeature +import org.elasticsearch.http.{HttpPreRequest, HttpServerTransport} +import org.elasticsearch.index.IndexModule +import org.elasticsearch.index.mapper.IgnoredFieldMapper +import org.elasticsearch.indices.breaker.CircuitBreakerService +import org.elasticsearch.plugins.ActionPlugin.ActionHandler +import org.elasticsearch.plugins._ +import org.elasticsearch.rest.{RestController, RestHandler} +import org.elasticsearch.telemetry.tracing.Tracer +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.netty4.{Netty4Utils, SharedGroupFactory} +import org.elasticsearch.transport.{Transport, TransportInterceptor} +import org.elasticsearch.xcontent.NamedXContentRegistry +import tech.beshu.ror.boot.{EsInitListener, SecurityProviderConfiguratorForFips} +import tech.beshu.ror.buildinfo.LogPluginBuildInfoMessage +import tech.beshu.ror.configuration.EnvironmentConfig +import tech.beshu.ror.constants +import tech.beshu.ror.es.actions.rradmin.rest.RestRRAdminAction +import tech.beshu.ror.es.actions.rradmin.{RRAdminActionType, TransportRRAdminAction} +import tech.beshu.ror.es.actions.rrauditevent.rest.RestRRAuditEventAction +import tech.beshu.ror.es.actions.rrauditevent.{RRAuditEventActionType, TransportRRAuditEventAction} +import tech.beshu.ror.es.actions.rrauthmock.rest.RestRRAuthMockAction +import tech.beshu.ror.es.actions.rrauthmock.{RRAuthMockActionType, TransportRRAuthMockAction} +import tech.beshu.ror.es.actions.rrconfig.rest.RestRRConfigAction +import tech.beshu.ror.es.actions.rrconfig.{RRConfigActionType, TransportRRConfigAction} +import tech.beshu.ror.es.actions.rrmetadata.rest.RestRRUserMetadataAction +import tech.beshu.ror.es.actions.rrmetadata.{RRUserMetadataActionType, TransportRRUserMetadataAction} +import tech.beshu.ror.es.actions.rrtestconfig.rest.RestRRTestConfigAction +import tech.beshu.ror.es.actions.rrtestconfig.{RRTestConfigActionType, TransportRRTestConfigAction} +import tech.beshu.ror.es.actions.wrappers._cat.{RorWrappedCatActionType, TransportRorWrappedCatAction} +import tech.beshu.ror.es.actions.wrappers._upgrade.{RorWrappedUpgradeActionType, TransportRorWrappedUpgradeAction} +import tech.beshu.ror.es.dlsfls.RoleIndexSearcherWrapper +import tech.beshu.ror.es.ssl.{SSLNetty4HttpServerTransport, SSLNetty4InternodeServerTransport} +import tech.beshu.ror.es.utils.{ChannelInterceptingRestHandlerDecorator, EsPatchVerifier, RemoteClusterServiceSupplier} +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged +import tech.beshu.ror.utils.SetOnce + +import java.nio.file.Path +import java.util +import java.util.function.{BiConsumer, Predicate, Supplier} +import scala.annotation.nowarn +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ +import scala.language.postfixOps + +@Inject +@nowarn("cat=deprecation") +class ReadonlyRestPlugin(s: Settings, p: Path) + extends Plugin + with ScriptPlugin + with ActionPlugin + with IngestPlugin + with NetworkPlugin + with ClusterPlugin { + + LogPluginBuildInfoMessage() + EsPatchVerifier.verify(s) + + constants.FIELDS_ALWAYS_ALLOW.add(IgnoredFieldMapper.NAME) + // ES uses Netty underlying and Finch also uses it under the hood. Seems that ES has reimplemented own available processor + // flag check, which is also done by Netty. So, we need to set it manually before ES and Finch, otherwise we will + // experience 'java.lang.IllegalStateException: availableProcessors is already set to [x], rejecting [x]' exception + doPrivileged { + Netty4Utils.setAvailableProcessors(EsExecutors.NODE_PROCESSORS_SETTING.get(s).roundDown()) + } + + private implicit val environmentConfig: EnvironmentConfig = EnvironmentConfig.default + + private val environment = new Environment(s, p) + private val timeout: FiniteDuration = 10 seconds + private val rorEsConfig = ReadonlyRestEsConfig + .load(environment.configFile) + .map(_.fold(e => throw new ElasticsearchException(e.message), identity)) + .runSyncUnsafe(timeout)(Scheduler.global, CanBlock.permit) + private val esInitListener = new EsInitListener + private val groupFactory = new SetOnce[SharedGroupFactory] + + private var ilaf: IndexLevelActionFilter = _ + + SecurityProviderConfiguratorForFips.configureIfRequired(rorEsConfig.fipsConfig) + + override def createComponents(services: Plugin.PluginServices): util.Collection[_] = { + doPrivileged { + val client = services.client() + val repositoriesServiceSupplier = services.repositoriesServiceSupplier() + ilaf = new IndexLevelActionFilter( + client.settings().get("node.name"), + services.clusterService(), + client.asInstanceOf[NodeClient], + services.threadPool(), + environment, + new RemoteClusterServiceSupplier(repositoriesServiceSupplier), + () => Some(repositoriesServiceSupplier.get()), + esInitListener, + rorEsConfig + ) + } + List.empty[AnyRef].asJava + } + + override def getActionFilters: util.List[ActionFilter] = { + List[ActionFilter](ilaf).asJava + } + + override def getTaskHeaders: util.Collection[String] = { + List(constants.FIELDS_TRANSIENT).asJava + } + + override def onIndexModule(indexModule: IndexModule): Unit = { + import tech.beshu.ror.es.utils.IndexModuleOps._ + indexModule.overwrite(RoleIndexSearcherWrapper.instance) + } + + override def getSettings: util.List[Setting[_]] = { + List[Setting[_]](Setting.groupSetting("readonlyrest.", Setting.Property.Dynamic, Setting.Property.NodeScope)).asJava + } + + override def getHttpTransports(settings: Settings, + threadPool: ThreadPool, + bigArrays: BigArrays, + pageCacheRecycler: PageCacheRecycler, + circuitBreakerService: CircuitBreakerService, + xContentRegistry: NamedXContentRegistry, + networkService: NetworkService, + dispatcher: HttpServerTransport.Dispatcher, + perRequestThreadContext: BiConsumer[HttpPreRequest, ThreadContext], + clusterSettings: ClusterSettings, + tracer: Tracer): util.Map[String, Supplier[HttpServerTransport]] = { + rorEsConfig + .sslConfig + .externalSsl + .map(ssl => + "ssl_netty4" -> new Supplier[HttpServerTransport] { + override def get(): HttpServerTransport = new SSLNetty4HttpServerTransport(settings, networkService, threadPool, xContentRegistry, dispatcher, ssl, clusterSettings, getSharedGroupFactory(settings), tracer, rorEsConfig.fipsConfig.isSslFipsCompliant) + } + ) + .toMap + .asJava + } + + override def getTransports(settings: Settings, + threadPool: ThreadPool, + pageCacheRecycler: PageCacheRecycler, + circuitBreakerService: CircuitBreakerService, + namedWriteableRegistry: NamedWriteableRegistry, + networkService: NetworkService): util.Map[String, Supplier[Transport]] = { + rorEsConfig + .sslConfig + .interNodeSsl + .map(ssl => + "ror_ssl_internode" -> new Supplier[Transport] { + override def get(): Transport = new SSLNetty4InternodeServerTransport(settings, threadPool, pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, networkService, ssl, getSharedGroupFactory(settings), rorEsConfig.fipsConfig.isSslFipsCompliant) + } + ) + .toMap + .asJava + } + + private def getSharedGroupFactory(settings: Settings): SharedGroupFactory = { + this.groupFactory.getOrElse(new SharedGroupFactory(settings)) + .ensuring(_.getSettings == settings, "Different settings than originally provided") + } + + override def close(): Unit = { + ilaf.stop() + } + + override def getActions: util.List[ActionPlugin.ActionHandler[_ <: ActionRequest, _ <: ActionResponse]] = { + List[ActionPlugin.ActionHandler[_ <: ActionRequest, _ <: ActionResponse]]( + new ActionHandler(RRAdminActionType.instance, classOf[TransportRRAdminAction]), + new ActionHandler(RRAuthMockActionType.instance, classOf[TransportRRAuthMockAction]), + new ActionHandler(RRTestConfigActionType.instance, classOf[TransportRRTestConfigAction]), + new ActionHandler(RRConfigActionType.instance, classOf[TransportRRConfigAction]), + new ActionHandler(RRUserMetadataActionType.instance, classOf[TransportRRUserMetadataAction]), + new ActionHandler(RRAuditEventActionType.instance, classOf[TransportRRAuditEventAction]), + // wrappers + new ActionHandler(RorWrappedCatActionType.instance, classOf[TransportRorWrappedCatAction]), + new ActionHandler(RorWrappedUpgradeActionType.instance, classOf[TransportRorWrappedUpgradeAction]), + ).asJava + } + + override def getRestHandlers(settings: Settings, + namedWriteableRegistry: NamedWriteableRegistry, + restController: RestController, + clusterSettings: ClusterSettings, + indexScopedSettings: IndexScopedSettings, + settingsFilter: SettingsFilter, + indexNameExpressionResolver: IndexNameExpressionResolver, + nodesInCluster: Supplier[DiscoveryNodes], + clusterSupportsFeature: Predicate[NodeFeature]): util.Collection[RestHandler] = { + import tech.beshu.ror.es.utils.RestControllerOps._ + restController.decorateRestHandlersWith(ChannelInterceptingRestHandlerDecorator.create) + List[RestHandler]( + new RestRRAdminAction(), + new RestRRAuthMockAction(), + new RestRRTestConfigAction(), + new RestRRConfigAction(nodesInCluster), + new RestRRUserMetadataAction(), + new RestRRAuditEventAction() + ).asJava + } + + override def getTransportInterceptors(namedWriteableRegistry: NamedWriteableRegistry, + threadContext: ThreadContext): util.List[TransportInterceptor] = { + List[TransportInterceptor](new RorTransportInterceptor(threadContext, s.get("node.name"))).asJava + } + + override def onNodeStarted(): Unit = { + super.onNodeStarted() + doPrivileged { + esInitListener.onEsReady() + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/ResponseFieldsFiltering.scala b/es813x/src/main/scala/tech/beshu/ror/es/ResponseFieldsFiltering.scala new file mode 100644 index 0000000000..f88f5f8106 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/ResponseFieldsFiltering.scala @@ -0,0 +1,91 @@ +/* + * 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 + +import monix.execution.atomic.Atomic +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler +import org.elasticsearch.rest.RestResponse +import org.elasticsearch.transport.BytesRefRecycler +import org.elasticsearch.transport.netty4.Netty4WriteThrottlingHandler +import org.elasticsearch.xcontent.cbor.CborXContent +import org.elasticsearch.xcontent.json.JsonXContent +import org.elasticsearch.xcontent.smile.SmileXContent +import org.elasticsearch.xcontent.yaml.YamlXContent +import org.elasticsearch.xcontent.{NamedXContentRegistry, XContentBuilder, XContentParserConfiguration, XContentType} +import tech.beshu.ror.accesscontrol.domain.ResponseFieldsFiltering.{AccessMode, ResponseFieldsRestrictions} + +import scala.jdk.CollectionConverters._ + +trait ResponseFieldsFiltering { + this: Logging => + + private val responseFieldsRestrictions: Atomic[Option[ResponseFieldsRestrictions]] = Atomic(None: Option[ResponseFieldsRestrictions]) + + def setResponseFieldRestrictions(responseFieldsRestrictions: ResponseFieldsRestrictions): Unit = { + this.responseFieldsRestrictions.set(Some(responseFieldsRestrictions)) + } + + protected def filterRestResponse(response: RestResponse): RestResponse = { + responseFieldsRestrictions.get() match { + case Some(fieldsRestrictions) => + filterRestResponse(response, fieldsRestrictions) + case None => + response + } + } + + private def filterRestResponse(response: RestResponse, fieldsRestrictions: ResponseFieldsRestrictions): RestResponse = { + val (includes, excludes) = fieldsRestrictions.mode match { + case AccessMode.Whitelist => + (fieldsRestrictions.responseFields.map(_.value.value), Set.empty[String]) + case AccessMode.Blacklist => + (Set.empty[String], fieldsRestrictions.responseFields.map(_.value.value)) + } + val xContent = + if(response.contentType().contains(XContentType.JSON.mediaTypeWithoutParameters())) JsonXContent.jsonXContent + else if (response.contentType().contains(XContentType.YAML.mediaTypeWithoutParameters())) YamlXContent.yamlXContent + else if (response.contentType().contains(XContentType.CBOR.mediaTypeWithoutParameters())) CborXContent.cborXContent + else if (response.contentType().contains(XContentType.SMILE.mediaTypeWithoutParameters())) SmileXContent.smileXContent + else throw new IllegalStateException("Unknown response content type") + + val contentStreamInput = (Option(response.content()), Option(response.chunkedContent())) match { + case (Some(content), None) => + content.streamInput() + case (None, Some(chunkedContent)) => + chunkedContent + .encodeChunk(Netty4WriteThrottlingHandler.MAX_BYTES_PER_WRITE, BytesRefRecycler.NON_RECYCLING_INSTANCE) + .streamInput() + case (Some(_), Some(_)) => + throw new IllegalStateException("Content and ChunkedContent should not be Some at the same time") + case (None, None) => + throw new IllegalStateException("Content and ChunkedContent should not be None at the same time") + } + + val parser = xContent.createParser( + XContentParserConfiguration.EMPTY + .withDeprecationHandler(LoggingDeprecationHandler.INSTANCE) + .withRegistry(NamedXContentRegistry.EMPTY), + contentStreamInput + ) + + val contentBuilder = XContentBuilder.builder(xContent.`type`(), includes.asJava, excludes.asJava) + contentBuilder.copyCurrentStructure(parser) + contentBuilder.flush() + new RestResponse(response.status(), contentBuilder) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/RorRestChannel.scala b/es813x/src/main/scala/tech/beshu/ror/es/RorRestChannel.scala new file mode 100644 index 0000000000..385274a67a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/RorRestChannel.scala @@ -0,0 +1,32 @@ +/* + * 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 + +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.rest.{AbstractRestChannel, RestChannel, RestResponse} +import tech.beshu.ror.es.utils.ThreadRepo + +class RorRestChannel(underlying: RestChannel) + extends AbstractRestChannel(underlying.request(), true) + with ResponseFieldsFiltering + with Logging { + + override def sendResponse(response: RestResponse): Unit = { + ThreadRepo.removeRestChannel(this) + underlying.sendResponse(filterRestResponse(response)) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/RorTransportInterceptor.scala b/es813x/src/main/scala/tech/beshu/ror/es/RorTransportInterceptor.scala new file mode 100644 index 0000000000..30814dc5b6 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/RorTransportInterceptor.scala @@ -0,0 +1,40 @@ +/* + * 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 + +import org.elasticsearch.common.util.concurrent.ThreadContext +import org.elasticsearch.transport._ +import tech.beshu.ror.accesscontrol.domain.Action +import tech.beshu.ror.es.utils.ThreadContextOps._ + +class RorTransportInterceptor(threadContext: ThreadContext, nodeName: String) + extends TransportInterceptor { + + override def interceptSender(sender: TransportInterceptor.AsyncSender): TransportInterceptor.AsyncSender = + new TransportInterceptor.AsyncSender { + override def sendRequest[T <: TransportResponse](connection: Transport.Connection, + action: String, + request: TransportRequest, + options: TransportRequestOptions, + handler: TransportResponseHandler[T]): Unit = { + if(Action.isInternal(action) || Action.isMonitorState(action)) { + threadContext.addSystemAuthenticationHeader(nodeName) + } + sender.sendRequest(connection, action, request, options, handler) + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/package.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/package.scala new file mode 100644 index 0000000000..424ad185cb --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/package.scala @@ -0,0 +1,26 @@ +/* + * 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 + +import org.elasticsearch.action.ActionRequest +import tech.beshu.ror.accesscontrol.request.LoggedUserSupport + +package object actions { + trait RorActionRequest extends LoggedUserSupport { + this: ActionRequest => + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminActionHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminActionHandler.scala new file mode 100644 index 0000000000..d204191726 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminActionHandler.scala @@ -0,0 +1,60 @@ +/* + * 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.actions.rradmin + +import cats.implicits.toShow +import monix.execution.Scheduler +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.ActionListener +import tech.beshu.ror.RequestId +import tech.beshu.ror.api.ConfigApi.ConfigResponse +import tech.beshu.ror.boot.RorSchedulers +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged +import tech.beshu.ror.utils.RorInstanceSupplier + +class RRAdminActionHandler() extends Logging { + + private implicit val adminRestApiScheduler: Scheduler = RorSchedulers.restApiScheduler + + def handle(request: RRAdminRequest, listener: ActionListener[RRAdminResponse]): Unit = { + getApi match { + case Some(api) => doPrivileged { + implicit val requestId: RequestId = request.requestContextId + api + .call(request.getAdminRequest) + .runAsync { response => + handle(response, listener) + } + } + case None => + listener.onFailure(new Exception("Config API is not available")) + } + } + + private def handle(result: Either[Throwable, ConfigResponse], + listener: ActionListener[RRAdminResponse]) + (implicit requestId: RequestId): Unit = result match { + case Right(response) => + listener.onResponse(new RRAdminResponse(response)) + case Left(ex) => + logger.error(s"[${requestId.show}] RRAdminAction internal error", ex) + listener.onFailure(new Exception(ex)) + } + + private def getApi = + RorInstanceSupplier.get().map(_.configApi) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminActionType.scala new file mode 100644 index 0000000000..41235dec4e --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminActionType.scala @@ -0,0 +1,31 @@ +/* + * 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.actions.rradmin + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable +import tech.beshu.ror.accesscontrol.domain.Action.RorAction + +class RRAdminActionType extends ActionType[RRAdminResponse](RRAdminActionType.name) + +object RRAdminActionType { + val name: String = RorAction.RorOldConfigAction.value + val instance = new RRAdminActionType() + final case object RRAdminActionCannotBeTransported extends Exception + def exceptionReader[A]: Writeable.Reader[A] = + _ => throw RRAdminActionCannotBeTransported +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminRequest.scala new file mode 100644 index 0000000000..0ed059f03d --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminRequest.scala @@ -0,0 +1,59 @@ +/* + * 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.actions.rradmin + +import org.elasticsearch.action.{ActionRequest, ActionRequestValidationException} +import org.elasticsearch.rest.RestRequest +import org.elasticsearch.rest.RestRequest.Method.{GET, POST} +import tech.beshu.ror.api.ConfigApi +import tech.beshu.ror.es.actions.RorActionRequest +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.{constants, RequestId} + +class RRAdminRequest(adminApiRequest: ConfigApi.ConfigRequest, + esRestRequest: RestRequest) extends ActionRequest with RorActionRequest { + + val getAdminRequest: ConfigApi.ConfigRequest = adminApiRequest + lazy val requestContextId: RequestId = RequestId(s"${esRestRequest.hashCode()}-${this.hashCode()}") + + override def validate(): ActionRequestValidationException = null +} + +object RRAdminRequest { + + def createFrom(request: RestRequest): RRAdminRequest = { + val requestType = (request.uri().addTrailingSlashIfNotPresent(), request.method()) match { + case (constants.FORCE_RELOAD_CONFIG_PATH, POST) => + ConfigApi.ConfigRequest.Type.ForceReload + case (constants.PROVIDE_FILE_CONFIG_PATH, GET) => + ConfigApi.ConfigRequest.Type.ProvideFileConfig + case (constants.PROVIDE_INDEX_CONFIG_PATH, GET) => + ConfigApi.ConfigRequest.Type.ProvideIndexConfig + case (constants.UPDATE_INDEX_CONFIG_PATH, POST) => + ConfigApi.ConfigRequest.Type.UpdateIndexConfig + case (unknownUri, unknownMethod) => + throw new IllegalStateException(s"Unknown request: $unknownMethod $unknownUri") + } + new RRAdminRequest( + new ConfigApi.ConfigRequest( + requestType, + request.content.utf8ToString + ), + request + ) + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminResponse.scala new file mode 100644 index 0000000000..89f067e651 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/RRAdminResponse.scala @@ -0,0 +1,77 @@ +/* + * 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.actions.rradmin + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput +import org.elasticsearch.rest.RestStatus +import org.elasticsearch.xcontent.{ToXContent, XContentBuilder} +import tech.beshu.ror.api.ConfigApi +import tech.beshu.ror.api.ConfigApi.ConfigResponse._ +import tech.beshu.ror.api.ConfigApi._ +import tech.beshu.ror.es.utils.StatusToXContentObject + +class RRAdminResponse(response: ConfigApi.ConfigResponse) + extends ActionResponse with StatusToXContentObject { + + override def toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder = { + response match { + case forceReloadConfig: ConfigResponse.ForceReloadConfig => forceReloadConfig match { + case ForceReloadConfig.Success(message) => addResponseJson(builder, response.status, message) + case ForceReloadConfig.Failure(message) => addResponseJson(builder, response.status, message) + } + case provideIndexConfig: ConfigResponse.ProvideIndexConfig => provideIndexConfig match { + case ProvideIndexConfig.Config(rawConfig) => addResponseJson(builder, response.status, rawConfig) + case ProvideIndexConfig.ConfigNotFound(message) => addResponseJson(builder, response.status, message) + case ProvideIndexConfig.Failure(message) => addResponseJson(builder, response.status, message) + } + case provideFileConfig: ConfigResponse.ProvideFileConfig => provideFileConfig match { + case ProvideFileConfig.Config(rawConfig) => addResponseJson(builder, response.status, rawConfig) + case ProvideFileConfig.Failure(message) => addResponseJson(builder, response.status, message) + } + case updateIndexConfig: ConfigResponse.UpdateIndexConfig => updateIndexConfig match { + case UpdateIndexConfig.Success(message) => addResponseJson(builder, response.status, message) + case UpdateIndexConfig.Failure(message) => addResponseJson(builder, response.status, message) + } + case failure: ConfigResponse.Failure => failure match { + case Failure.BadRequest(message) => addResponseJson(builder, response.status, message) + } + } + builder + } + + override def writeTo(out: StreamOutput): Unit = () + + override def status: RestStatus = { + response match { + case _: ForceReloadConfig => RestStatus.OK + case _: ProvideIndexConfig => RestStatus.OK + case _: ProvideFileConfig => RestStatus.OK + case _: UpdateIndexConfig => RestStatus.OK + case failure: Failure => failure match { + case Failure.BadRequest(_) => RestStatus.BAD_REQUEST + } + } + } + + private def addResponseJson(builder: XContentBuilder, status: String, message: String): Unit = { + builder.startObject + builder.field("status", status) + builder.field("message", message) + builder.endObject + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/TransportRRAdminAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/TransportRRAdminAction.scala new file mode 100644 index 0000000000..0404351400 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/TransportRRAdminAction.scala @@ -0,0 +1,49 @@ +/* + * 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.actions.rradmin + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.support.{ActionFilters, HandledTransportAction} +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService + +import java.util.concurrent.Executor +import scala.annotation.nowarn + +class TransportRRAdminAction(transportService: TransportService, + actionFilters: ActionFilters, + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends HandledTransportAction[RRAdminRequest, RRAdminResponse]( + RRAdminActionType.name, transportService, actionFilters, RRAdminActionType.exceptionReader[RRAdminRequest], executor + ) { + + @Inject + def this(transportService: TransportService, + actionFilters: ActionFilters, + threadPool: ThreadPool) = { + this(transportService, actionFilters, threadPool.executor(ThreadPool.Names.GENERIC), ()) + } + + private val handler = new RRAdminActionHandler() + + override def doExecute(task: Task, request: RRAdminRequest, listener: ActionListener[RRAdminResponse]): Unit = { + handler.handle(request, listener) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/rest/RestRRAdminAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/rest/RestRRAdminAction.scala new file mode 100644 index 0000000000..a1f2272930 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rradmin/rest/RestRRAdminAction.scala @@ -0,0 +1,52 @@ +/* + * 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.actions.rradmin.rest + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.rest.BaseRestHandler.RestChannelConsumer +import org.elasticsearch.rest.RestHandler.Route +import org.elasticsearch.rest.RestRequest.Method._ +import org.elasticsearch.rest._ +import tech.beshu.ror.constants +import tech.beshu.ror.es.actions.rradmin.{RRAdminActionType, RRAdminRequest, RRAdminResponse} +import tech.beshu.ror.es.utils.RestToXContentWithStatusListener + +import java.util +import scala.jdk.CollectionConverters._ + +@Inject +class RestRRAdminAction() + extends BaseRestHandler with RestHandler { + + override def routes(): util.List[Route] = List( + new Route(POST, constants.FORCE_RELOAD_CONFIG_PATH), + new Route(GET, constants.PROVIDE_FILE_CONFIG_PATH), + new Route(GET, constants.PROVIDE_INDEX_CONFIG_PATH), + new Route(POST, constants.UPDATE_INDEX_CONFIG_PATH), + ).asJava + + override val getName: String = "ror-admin-handler" + + override def prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer = new RestChannelConsumer { + private val rorAdminRequest = RRAdminRequest.createFrom(request) + + override def accept(channel: RestChannel): Unit = { + client.execute(new RRAdminActionType, rorAdminRequest, new RestToXContentWithStatusListener[RRAdminResponse](channel)) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventActionHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventActionHandler.scala new file mode 100644 index 0000000000..540bbcc7ea --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventActionHandler.scala @@ -0,0 +1,28 @@ +/* + * 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.actions.rrauditevent + +import org.elasticsearch.action.ActionListener + +import scala.annotation.nowarn + +object RRAuditEventActionHandler { + + def handle(@nowarn("cat=unused") request: RRAuditEventRequest, listener: ActionListener[RRAuditEventResponse]): Unit = { + listener.onResponse(new RRAuditEventResponse()) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventActionType.scala new file mode 100644 index 0000000000..24102b090b --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventActionType.scala @@ -0,0 +1,32 @@ +/* + * 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.actions.rrauditevent + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable +import tech.beshu.ror.accesscontrol.domain.Action.RorAction + +class RRAuditEventActionType extends ActionType[RRAuditEventResponse](RRAuditEventActionType.name) + +object RRAuditEventActionType { + val name: String = RorAction.RorAuditEventAction.value + val instance = new RRAuditEventActionType() + + final case object RRAuditEventActionTypeBeTransported extends Exception + + private [rrauditevent] def exceptionReader[A]: Writeable.Reader[A] = _ => throw RRAuditEventActionTypeBeTransported +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventRequest.scala new file mode 100644 index 0000000000..c86b4b1cd9 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventRequest.scala @@ -0,0 +1,25 @@ +/* + * 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.actions.rrauditevent + +import org.elasticsearch.action.{ActionRequest, ActionRequestValidationException} +import org.json.JSONObject +import tech.beshu.ror.es.actions.RorActionRequest + +class RRAuditEventRequest(val auditEvents: JSONObject) extends ActionRequest with RorActionRequest { + override def validate(): ActionRequestValidationException = null +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventResponse.scala new file mode 100644 index 0000000000..4a26dc96df --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/RRAuditEventResponse.scala @@ -0,0 +1,28 @@ +/* + * 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.actions.rrauditevent + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput +import org.elasticsearch.xcontent.{ToXContent, ToXContentObject, XContentBuilder} + +class RRAuditEventResponse extends ActionResponse with ToXContentObject { + + override def writeTo(out: StreamOutput): Unit = () + + override def toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder = builder +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/TransportRRAuditEventAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/TransportRRAuditEventAction.scala new file mode 100644 index 0000000000..aedd09e314 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/TransportRRAuditEventAction.scala @@ -0,0 +1,47 @@ +/* + * 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.actions.rrauditevent + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.support.{ActionFilters, HandledTransportAction} +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService + +import java.util.concurrent.Executor +import scala.annotation.nowarn + +class TransportRRAuditEventAction(transportService: TransportService, + actionFilters: ActionFilters, + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends HandledTransportAction[RRAuditEventRequest, RRAuditEventResponse]( + RRAuditEventActionType.name, transportService, actionFilters, RRAuditEventActionType.exceptionReader[RRAuditEventRequest], executor + ) { + + @Inject + def this(transportService: TransportService, + actionFilters: ActionFilters, + threadPool: ThreadPool) = + this(transportService, actionFilters, threadPool.executor(ThreadPool.Names.GENERIC), ()) + + override def doExecute(task: Task, request: RRAuditEventRequest, + listener: ActionListener[RRAuditEventResponse]): Unit = { + RRAuditEventActionHandler.handle(request, listener) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/rest/RestRRAuditEventAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/rest/RestRRAuditEventAction.scala new file mode 100644 index 0000000000..cc69a6b3ba --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/rest/RestRRAuditEventAction.scala @@ -0,0 +1,90 @@ +/* + * 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.actions.rrauditevent.rest + +import java.util + +import org.elasticsearch.ElasticsearchException +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.common.xcontent.XContentHelper +import org.elasticsearch.rest.BaseRestHandler.RestChannelConsumer +import org.elasticsearch.rest.RestHandler.Route +import org.elasticsearch.rest.RestRequest.Method.POST +import org.elasticsearch.rest._ +import org.json.JSONObject +import squants.information.{Bytes, Information} +import tech.beshu.ror.constants +import tech.beshu.ror.es.actions.rrauditevent.{RRAuditEventActionType, RRAuditEventRequest} + +import scala.jdk.CollectionConverters._ +import scala.util.Try + +@Inject +class RestRRAuditEventAction() + extends BaseRestHandler with RestHandler { + + override def routes(): util.List[Route] = List( + new Route(POST, constants.AUDIT_EVENT_COLLECTOR_PATH) + ).asJava + + override val getName: String = "ror-audit-event-collector-handler" + + override def prepareRequest(request: RestRequest, + client: NodeClient): RestChannelConsumer = new RestChannelConsumer { + private val rorAuditRequest = for { + _ <- validateContentSize(request) + json <- validateBodyJson(request) + } yield new RRAuditEventRequest(json) + + override def accept(channel: RestChannel): Unit = { + val listener = new RestRRAuditEventActionResponseBuilder(channel) + rorAuditRequest match { + case Right(req) => + client.execute(new RRAuditEventActionType, req, listener) + case Left(error) => + listener.onFailure(error) + } + } + } + + private def validateContentSize(request: RestRequest) = { + Either.cond( + request.content().length() <= constants.MAX_AUDIT_EVENT_REQUEST_CONTENT_IN_BYTES, + (), + new AuditEventRequestPayloadTooLarge(Bytes(constants.MAX_AUDIT_EVENT_REQUEST_CONTENT_IN_BYTES.toInt)) + ) + } + + private def validateBodyJson(request: RestRequest) = Try { + if (request.hasContent) { + new JSONObject(XContentHelper.convertToMap(request.requiredContent(), false, request.getXContentType).v2()) + } else { + new JSONObject() + } + }.toEither.left.map(_ => new AuditEventBadRequest) + + private class AuditEventBadRequest extends ElasticsearchException("Content malformed") { + override def status(): RestStatus = RestStatus.BAD_REQUEST + } + + private class AuditEventRequestPayloadTooLarge(maxContentSize: Information) + extends ElasticsearchException(s"Max request content allowed = ${maxContentSize.toKilobits}KB") { + override def status(): RestStatus = RestStatus.REQUEST_ENTITY_TOO_LARGE + } + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/rest/RestRRAuditEventActionResponseBuilder.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/rest/RestRRAuditEventActionResponseBuilder.scala new file mode 100644 index 0000000000..7be52c7a5d --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauditevent/rest/RestRRAuditEventActionResponseBuilder.scala @@ -0,0 +1,30 @@ +/* + * 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.actions.rrauditevent.rest + +import org.elasticsearch.xcontent.XContentBuilder +import org.elasticsearch.rest.action.RestBuilderListener +import org.elasticsearch.rest.{RestChannel, RestResponse, RestStatus} +import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventResponse + +class RestRRAuditEventActionResponseBuilder(channel: RestChannel) + extends RestBuilderListener[RRAuditEventResponse](channel) { + + override def buildResponse(response: RRAuditEventResponse, builder: XContentBuilder): RestResponse = { + new RestResponse(RestStatus.NO_CONTENT, "") + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockActionHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockActionHandler.scala new file mode 100644 index 0000000000..195ca40a66 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockActionHandler.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.actions.rrauthmock + +import cats.implicits.toShow +import monix.execution.Scheduler +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.ActionListener +import tech.beshu.ror.RequestId +import tech.beshu.ror.api.AuthMockApi.AuthMockResponse +import tech.beshu.ror.boot.RorSchedulers +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged +import tech.beshu.ror.utils.RorInstanceSupplier + +class RRAuthMockActionHandler() extends Logging { + + private implicit val rorRestApiScheduler: Scheduler = RorSchedulers.restApiScheduler + + def handle(request: RRAuthMockRequest, listener: ActionListener[RRAuthMockResponse]): Unit = { + getApi match { + case Some(api) => doPrivileged { + implicit val requestId: RequestId = request.requestContextId + api + .call(request.getAuthMockRequest) + .runAsync { response => + handle(response, listener) + } + } + case None => + listener.onFailure(new Exception("AuthMock API is not available")) + } + } + + private def handle(result: Either[Throwable, AuthMockResponse], + listener: ActionListener[RRAuthMockResponse]) + (implicit requestId: RequestId): Unit = { + result match { + case Right(response) => + listener.onResponse(new RRAuthMockResponse(response)) + case Left(ex) => + logger.error(s"[${requestId.show}] RRAuthMock internal error", ex) + listener.onFailure(new Exception(ex)) + } + } + + private def getApi = + RorInstanceSupplier.get().map(_.authMockApi) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockActionType.scala new file mode 100644 index 0000000000..f214cf166b --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockActionType.scala @@ -0,0 +1,31 @@ +/* + * 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.actions.rrauthmock + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable +import tech.beshu.ror.accesscontrol.domain.Action.RorAction + +class RRAuthMockActionType extends ActionType[RRAuthMockResponse](RRAuthMockActionType.name) +object RRAuthMockActionType { + val name: String = RorAction.RorAuthMockAction.value + val instance = new RRAuthMockActionType() + + final case object RRAuthMockActionCannotBeTransported extends Exception + + private [rrauthmock] def exceptionReader[A]: Writeable.Reader[A] = _ => throw RRAuthMockActionCannotBeTransported +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockRequest.scala new file mode 100644 index 0000000000..53def24838 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockRequest.scala @@ -0,0 +1,55 @@ +/* + * 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.actions.rrauthmock + +import org.elasticsearch.action.{ActionRequest, ActionRequestValidationException} +import org.elasticsearch.rest.RestRequest +import org.elasticsearch.rest.RestRequest.Method.{GET, POST} +import tech.beshu.ror.api.AuthMockApi +import tech.beshu.ror.es.actions.RorActionRequest +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.{constants, RequestId} + +class RRAuthMockRequest(authMockApiRequest: AuthMockApi.AuthMockRequest, + esRestRequest: RestRequest) extends ActionRequest with RorActionRequest { + + val getAuthMockRequest: AuthMockApi.AuthMockRequest = authMockApiRequest + lazy val requestContextId: RequestId = RequestId(s"${esRestRequest.hashCode()}-${this.hashCode()}") + + override def validate(): ActionRequestValidationException = null +} + +object RRAuthMockRequest { + + def createFrom(request: RestRequest): RRAuthMockRequest = { + val requestType = (request.uri().addTrailingSlashIfNotPresent(), request.method()) match { + case (constants.PROVIDE_AUTH_MOCK_PATH, GET) => + AuthMockApi.AuthMockRequest.Type.ProvideAuthMock + case (constants.CONFIGURE_AUTH_MOCK_PATH, POST) => + AuthMockApi.AuthMockRequest.Type.UpdateAuthMock + case (unknownUri, unknownMethod) => + throw new IllegalStateException(s"Unknown request: $unknownMethod $unknownUri") + } + new RRAuthMockRequest( + new AuthMockApi.AuthMockRequest( + requestType, + request.content.utf8ToString + ), + request + ) + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockResponse.scala new file mode 100644 index 0000000000..64c4e6e0b0 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/RRAuthMockResponse.scala @@ -0,0 +1,152 @@ +/* + * 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.actions.rrauthmock + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput +import tech.beshu.ror.es.utils.StatusToXContentObject +import org.elasticsearch.rest.RestStatus +import org.elasticsearch.xcontent.{ToXContent, XContentBuilder} +import tech.beshu.ror.api.AuthMockApi +import tech.beshu.ror.api.AuthMockApi.AuthMockResponse.{Failure, ProvideAuthMock, UpdateAuthMock} +import tech.beshu.ror.api.AuthMockApi.AuthMockService.{ExternalAuthenticationService, ExternalAuthorizationService, LdapAuthorizationService, MockMode} +import tech.beshu.ror.api.AuthMockApi.{AuthMockResponse, AuthMockService} + +import scala.jdk.CollectionConverters._ + +class RRAuthMockResponse(response: AuthMockApi.AuthMockResponse) + extends ActionResponse with StatusToXContentObject { + + override def toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder = { + response match { + case mock: AuthMockResponse.ProvideAuthMock => mock match { + case ProvideAuthMock.CurrentAuthMocks(services) => currentServicesJson(builder, services) + case ProvideAuthMock.NotConfigured(message) => addResponseJson(builder, response.status, message) + case ProvideAuthMock.Invalidated(message) => addResponseJson(builder, response.status, message) + } + case mock: AuthMockResponse.UpdateAuthMock => mock match { + case UpdateAuthMock.Success(message) => addResponseJson(builder, response.status, message) + case UpdateAuthMock.NotConfigured(message) => addResponseJson(builder, response.status, message) + case UpdateAuthMock.Invalidated(message) => addResponseJson(builder, response.status, message) + case UpdateAuthMock.UnknownAuthServicesDetected(message) => addResponseJson(builder, response.status, message) + case UpdateAuthMock.Failed(message) => addResponseJson(builder, response.status, message) + } + case failure: Failure => failure match { + case Failure.BadRequest(message) => addResponseJson(builder, response.status, message) + } + } + builder + } + + override def writeTo(out: StreamOutput): Unit = () + + override def status: RestStatus = response match { + case _: AuthMockResponse.ProvideAuthMock => RestStatus.OK + case _: AuthMockResponse.UpdateAuthMock => RestStatus.OK + case failure: AuthMockResponse.Failure => failure match { + case Failure.BadRequest(_) => RestStatus.BAD_REQUEST + } + } + + private def addResponseJson(builder: XContentBuilder, status: String, message: String): Unit = { + builder.startObject + builder.field("status", status) + builder.field("message", message) + builder.endObject + } + + private def currentServicesJson(builder: XContentBuilder, services: List[AuthMockService]): Unit = { + builder.startObject + builder.field("status", response.status) + builder.startArray("services") + services.foreach { service => + buildForService(builder, service) + } + builder.endArray() + builder.endObject + } + + private def buildForService(builder: XContentBuilder, service: AuthMockService): Unit = { + service match { + case AuthMockService.LdapAuthorizationService(name, mock) => + builder.startObject() + builder.field("type", service.serviceType) + builder.field("name", name.value) + ldapMock(builder, mock) + builder.endObject() + case AuthMockService.ExternalAuthenticationService(name, mock) => + builder.startObject() + builder.field("type", service.serviceType) + builder.field("name", name.value) + externalAuthenticationMock(builder, mock) + builder.endObject() + case AuthMockService.ExternalAuthorizationService(name, mock) => + builder.startObject() + builder.field("type", service.serviceType) + builder.field("name", name.value) + externalAuthorizationMock(builder, mock) + builder.endObject() + } + } + + private def externalAuthorizationMock(builder: XContentBuilder, mock: MockMode[ExternalAuthorizationService.Mock]): Unit = mock match { + case MockMode.Enabled(configuredMock) => + builder.startObject("mock") + builder.startArray("users") + configuredMock.users.foreach { user => + builder.startObject() + builder.field("name", user.name.value) + builder.field("groups", user.groups.map(_.value).asJava) + builder.endObject() + } + builder.endArray() + builder.endObject() + case MockMode.NotConfigured => + builder.field("mock", "NOT_CONFIGURED") + } + + private def externalAuthenticationMock(builder: XContentBuilder, mock: MockMode[ExternalAuthenticationService.Mock]): Unit = mock match { + case MockMode.Enabled(configuredMock) => + builder.startObject("mock") + builder.startArray("users") + configuredMock.users.foreach { user => + builder.startObject() + builder.field("name", user.name.value) + builder.endObject() + } + builder.endArray() + builder.endObject() + case MockMode.NotConfigured => + builder.field("mock", "NOT_CONFIGURED") + } + + private def ldapMock(builder: XContentBuilder, mock: MockMode[LdapAuthorizationService.Mock]): Unit = mock match { + case MockMode.Enabled(configuredMock) => + builder.startObject("mock") + builder.startArray("users") + configuredMock.users.foreach { user => + builder.startObject() + builder.field("name", user.name.value) + builder.field("groups", user.groups.map(_.value).asJava) + builder.endObject() + } + builder.endArray() + builder.endObject() + case MockMode.NotConfigured => + builder.field("mock", "NOT_CONFIGURED") + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/TransportRRAuthMockAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/TransportRRAuthMockAction.scala new file mode 100644 index 0000000000..bc9087baa7 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/TransportRRAuthMockAction.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.es.actions.rrauthmock + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.support.{ActionFilters, HandledTransportAction} +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService + +import java.util.concurrent.Executor +import scala.annotation.nowarn + +class TransportRRAuthMockAction(transportService: TransportService, + actionFilters: ActionFilters, + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends HandledTransportAction[RRAuthMockRequest, RRAuthMockResponse]( + RRAuthMockActionType.name, transportService, actionFilters, RRAuthMockActionType.exceptionReader[RRAuthMockRequest], executor + ) { + + @Inject + def this(transportService: TransportService, + actionFilters: ActionFilters, + threadPool: ThreadPool) = + this(transportService, actionFilters, threadPool.executor(ThreadPool.Names.GENERIC), ()) + + private val handler = new RRAuthMockActionHandler() + + override def doExecute(task: Task, request: RRAuthMockRequest, listener: ActionListener[RRAuthMockResponse]): Unit = { + handler.handle(request, listener) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/rest/RestRRAuthMockAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/rest/RestRRAuthMockAction.scala new file mode 100644 index 0000000000..54eb65d87c --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrauthmock/rest/RestRRAuthMockAction.scala @@ -0,0 +1,51 @@ +/* + * 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.actions.rrauthmock.rest + +import java.util + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.rest.BaseRestHandler.RestChannelConsumer +import org.elasticsearch.rest.RestHandler.Route +import org.elasticsearch.rest.RestRequest.Method.{GET, POST} +import org.elasticsearch.rest.{BaseRestHandler, RestChannel, RestHandler, RestRequest} +import tech.beshu.ror.constants +import tech.beshu.ror.es.actions.rrauthmock.{RRAuthMockActionType, RRAuthMockRequest, RRAuthMockResponse} +import tech.beshu.ror.es.utils.RestToXContentWithStatusListener + +import scala.jdk.CollectionConverters._ + +@Inject +class RestRRAuthMockAction() + extends BaseRestHandler with RestHandler { + + override def routes(): util.List[Route] = List( + new Route(GET, constants.PROVIDE_AUTH_MOCK_PATH), + new Route(POST, constants.CONFIGURE_AUTH_MOCK_PATH) + ).asJava + + override val getName: String = "ror-auth-mock-handler" + + override def prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer = new RestChannelConsumer { + private val rorAuthMockRequest = RRAuthMockRequest.createFrom(request) + + override def accept(channel: RestChannel): Unit = { + client.execute(new RRAuthMockActionType, rorAuthMockRequest, new RestToXContentWithStatusListener[RRAuthMockResponse](channel)) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfig.java b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfig.java new file mode 100644 index 0000000000..332bb90e1f --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfig.java @@ -0,0 +1,65 @@ +/* + * 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.actions.rrconfig; + +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import tech.beshu.ror.configuration.loader.distributed.NodeConfig; +import tech.beshu.ror.configuration.loader.distributed.internode.NodeConfigSerializer; + +import java.io.IOException; + +public class RRConfig extends BaseNodeResponse { + private final NodeConfig nodeConfig; + + @Inject + public RRConfig(StreamInput in) throws IOException { + super(in); + this.nodeConfig = NodeConfigSerializer.parse(in.readString()); + } + + public RRConfig(DiscoveryNode discoveryNode, NodeConfig nodeConfig) { + super(discoveryNode); + this.nodeConfig = nodeConfig; + } + + public RRConfig(DiscoveryNode discoveryNode, StreamInput in) throws IOException { + super(discoveryNode); + this.nodeConfig = NodeConfigSerializer.parse(in.readString()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(NodeConfigSerializer.serialize(nodeConfig)); + } + + public NodeConfig getNodeConfig() { + return nodeConfig; + } + + @Override + public String toString() { + return "RRConfig{" + + "nodeConfig=" + nodeConfig + ", " + + "discoveryNode=" + getNode() + + '}'; + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigActionType.scala new file mode 100644 index 0000000000..942897ce23 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigActionType.scala @@ -0,0 +1,29 @@ +/* + * 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.actions.rrconfig + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable +import tech.beshu.ror.accesscontrol.domain.Action.RorAction + +class RRConfigActionType extends ActionType[RRConfigsResponse](RRConfigActionType.name) + +object RRConfigActionType { + val name: String = RorAction.RorConfigAction.value + val instance = new RRConfigActionType + val reader: Writeable.Reader[RRConfigsResponse] = new RRConfigsResponse(_) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigRequest.java b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigRequest.java new file mode 100644 index 0000000000..75d052eb16 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigRequest.java @@ -0,0 +1,49 @@ +/* + * 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.actions.rrconfig; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.transport.TransportRequest; +import tech.beshu.ror.configuration.loader.distributed.NodeConfigRequest; +import tech.beshu.ror.configuration.loader.distributed.internode.NodeConfigRequestSerializer; + +import java.io.IOException; + +public class RRConfigRequest extends TransportRequest { + private final NodeConfigRequest nodeConfigRequest; + + public RRConfigRequest(NodeConfigRequest nodeConfigRequest) { + super(); + this.nodeConfigRequest = nodeConfigRequest; + } + + public RRConfigRequest(StreamInput in) throws IOException { + super(in); + this.nodeConfigRequest = NodeConfigRequestSerializer.parse(in.readString()); + } + + public NodeConfigRequest getNodeConfigRequest() { + return nodeConfigRequest; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(NodeConfigRequestSerializer.serialize(this.nodeConfigRequest)); + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigsRequest.java b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigsRequest.java new file mode 100644 index 0000000000..39e7416033 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigsRequest.java @@ -0,0 +1,61 @@ +/* + * 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.actions.rrconfig; + +import org.elasticsearch.action.support.nodes.BaseNodesRequest; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import tech.beshu.ror.configuration.loader.distributed.NodeConfigRequest; +import tech.beshu.ror.configuration.loader.distributed.internode.NodeConfigRequestSerializer; + +import java.io.IOException; +import java.util.Arrays; + +public class RRConfigsRequest extends BaseNodesRequest { + + private final NodeConfigRequest nodeConfigRequest; + + @Inject + public RRConfigsRequest(StreamInput in) throws IOException { + super(in); + this.nodeConfigRequest = NodeConfigRequestSerializer.parse(in.readString()); + } + + public RRConfigsRequest(NodeConfigRequest nodeConfigRequest, DiscoveryNode... concreteNodes) { + super(concreteNodes); + this.nodeConfigRequest = nodeConfigRequest; + } + + public NodeConfigRequest getNodeConfigRequest() { + return nodeConfigRequest; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(NodeConfigRequestSerializer.serialize(this.nodeConfigRequest)); + } + + @Override + public String toString() { + return "RRConfigsRequest{" + + "concreteNodes=" + Arrays.asList(concreteNodes()) + + '}'; + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigsResponse.java b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigsResponse.java new file mode 100644 index 0000000000..28a9fbeafc --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/RRConfigsResponse.java @@ -0,0 +1,47 @@ +/* + * 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.actions.rrconfig; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.List; + +public class RRConfigsResponse extends BaseNodesResponse { + + protected RRConfigsResponse(StreamInput in) throws IOException { + super(in); + } + + public RRConfigsResponse(ClusterName clusterName, List nodes, List failures) { + super(clusterName, nodes, failures); + } + + @Override + protected List readNodesFrom(StreamInput in) throws IOException { + return in.readCollectionAsList(RRConfig::new); + } + + @Override + protected void writeNodesTo(StreamOutput out, List nodes) throws IOException { + out.writeCollection(nodes); + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/TransportRRConfigAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/TransportRRConfigAction.scala new file mode 100644 index 0000000000..3a800a6427 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/TransportRRConfigAction.scala @@ -0,0 +1,113 @@ +/* + * 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.actions.rrconfig + +import java.util +import cats.implicits._ +import org.elasticsearch.action.FailedNodeException +import org.elasticsearch.action.support.ActionFilters +import org.elasticsearch.action.support.nodes.TransportNodesAction +import org.elasticsearch.cluster.node.DiscoveryNode +import org.elasticsearch.cluster.service.ClusterService +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.common.io.stream.{StreamInput, Writeable} +import org.elasticsearch.env.Environment +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService +import tech.beshu.ror.configuration.EnvironmentConfig +import tech.beshu.ror.configuration.loader.distributed.{NodeConfig, RawRorConfigLoadingAction, Timeout} +import tech.beshu.ror.es.IndexJsonContentService +import tech.beshu.ror.es.services.EsIndexJsonContentService + +import java.util.concurrent.Executor +import scala.annotation.nowarn +import scala.concurrent.duration._ +import scala.language.postfixOps + +class TransportRRConfigAction(actionName: String, + clusterService: ClusterService, + transportService: TransportService, + actionFilters: ActionFilters, + env: Environment, + indexContentProvider: IndexJsonContentService, + nodeRequest: Writeable.Reader[RRConfigRequest], + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends TransportNodesAction[RRConfigsRequest, RRConfigsResponse, RRConfigRequest, RRConfig]( + actionName, + clusterService, + transportService, + actionFilters, + nodeRequest, + executor + ) { + + import tech.beshu.ror.boot.RorSchedulers.Implicits.rorRestApiScheduler + + private implicit val environmentConfig: EnvironmentConfig = EnvironmentConfig.default + + @Inject + def this(actionName: String, + threadPool: ThreadPool, + clusterService: ClusterService, + transportService: TransportService, + actionFilters: ActionFilters, + env: Environment, + indexContentProvider: EsIndexJsonContentService, + ) = + this( + RRConfigActionType.name, + clusterService, + transportService, + actionFilters, + env, + indexContentProvider, + new RRConfigRequest(_), + threadPool.executor(ThreadPool.Names.GENERIC), + () + ) + + override def newResponse(request: RRConfigsRequest, responses: util.List[RRConfig], failures: util.List[FailedNodeException]): RRConfigsResponse = { + new RRConfigsResponse(clusterService.getClusterName, responses, failures) + } + + override def newNodeResponse(streamInput: StreamInput, discoveryNode: DiscoveryNode): RRConfig = { + new RRConfig(discoveryNode, streamInput) + } + + override def newNodeRequest(request: RRConfigsRequest): RRConfigRequest = + new RRConfigRequest(request.getNodeConfigRequest) + + private def loadConfig() = + RawRorConfigLoadingAction + .load(env.configFile(), indexContentProvider) + .map(_.map(_.map(_.raw))) + + override def nodeOperation(request: RRConfigRequest, task: Task): RRConfig = { + val nodeRequest = request.getNodeConfigRequest + val nodeResponse = + loadConfig() + .runSyncUnsafe(toFiniteDuration(nodeRequest.timeout)) + new RRConfig(clusterService.localNode(), NodeConfig(nodeResponse)) + } + + private def toFiniteDuration(timeout: Timeout): FiniteDuration = timeout.nanos nanos + +} + + diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/rest/RestRRConfigAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/rest/RestRRConfigAction.scala new file mode 100644 index 0000000000..e2bf30ef89 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/rest/RestRRConfigAction.scala @@ -0,0 +1,70 @@ +/* + * 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.actions.rrconfig.rest + +import java.util +import java.util.function.Supplier + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.cluster.node.DiscoveryNodes +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.core.TimeValue +import org.elasticsearch.rest.BaseRestHandler.RestChannelConsumer +import org.elasticsearch.rest.RestHandler.Route +import org.elasticsearch.rest.RestRequest.Method.GET +import org.elasticsearch.rest._ +import tech.beshu.ror.constants +import tech.beshu.ror.configuration.loader.distributed.NodesResponse.NodeId +import tech.beshu.ror.configuration.loader.distributed.{NodeConfigRequest, Timeout} +import tech.beshu.ror.es.actions.rrconfig.{RRConfigActionType, RRConfigsRequest} + +import scala.jdk.CollectionConverters._ + +@Inject +class RestRRConfigAction(nodesInCluster: Supplier[DiscoveryNodes]) + extends BaseRestHandler { + + override def routes(): util.List[Route] = List( + new Route(GET, constants.MANAGE_ROR_CONFIG_PATH), + ).asJava + + override val getName: String = "ror-config-handler" + + override def prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer = { + val timeout = getTimeout(request, RestRRConfigAction.defaultTimeout) + val requestConfig = NodeConfigRequest( + timeout = Timeout(timeout.nanos()) + ) + channel => + client.execute( + new RRConfigActionType, + new RRConfigsRequest(requestConfig, nodes.toArray: _*), + new RestRRConfigActionResponseBuilder(NodeId(client.getLocalNodeId), channel) + ) + } + + private def getTimeout(request: RestRequest, default: TimeValue) = + request.paramAsTime("timeout", default) + + private def nodes = + nodesInCluster.get().asScala.toList + +} +object RestRRConfigAction { + private val defaultTimeout: TimeValue = toTimeValue(NodeConfigRequest.defaultTimeout) + private def toTimeValue(timeout: Timeout):TimeValue = TimeValue.timeValueNanos(timeout.nanos) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/rest/RestRRConfigActionResponseBuilder.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/rest/RestRRConfigActionResponseBuilder.scala new file mode 100644 index 0000000000..6b58a493d2 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrconfig/rest/RestRRConfigActionResponseBuilder.scala @@ -0,0 +1,65 @@ +/* + * 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.actions.rrconfig.rest + +import java.util.concurrent.TimeoutException + +import org.elasticsearch.action.FailedNodeException +import org.elasticsearch.xcontent.XContentBuilder +import org.elasticsearch.rest.action.RestBuilderListener +import org.elasticsearch.rest.{RestChannel, RestResponse, RestStatus} +import org.elasticsearch.transport.ActionNotFoundTransportException +import tech.beshu.ror.configuration.loader.distributed.NodesResponse +import tech.beshu.ror.configuration.loader.distributed.NodesResponse.{NodeError, NodeId, NodeResponse} +import tech.beshu.ror.es.actions.rrconfig.{RRConfig, RRConfigsResponse} + +import scala.jdk.CollectionConverters._ + +final class RestRRConfigActionResponseBuilder(localNode: NodeId, channel: RestChannel) + extends RestBuilderListener[RRConfigsResponse](channel) { + + override def buildResponse(response: RRConfigsResponse, builder: XContentBuilder): RestResponse = { + val nodeResponse = createNodesResponse(response) + new RestResponse(RestStatus.OK, nodeResponse.toJson) + } + + private def createNodesResponse(response: RRConfigsResponse) = + NodesResponse.create( + localNode = localNode, + responses = response.getNodes.asScala.toList.map(createNodeResponse), + failures = response.failures().asScala.toList.map(createNodeError), + ) + + private def createNodeResponse(config: RRConfig) = { + NodeResponse(NodeId(config.getNode.getId), config.getNodeConfig.loadedConfig) + } + + private def createNodeError(failedNodeException: FailedNodeException) = { + NodeError(NodeId(failedNodeException.nodeId()), createCause(failedNodeException)) + } + + private def createCause(failedNodeException: FailedNodeException): NodeError.Cause = { + failedNodeException.getRootCause match { + case _: TimeoutException => + NodeError.Timeout + case _: ActionNotFoundTransportException => + NodeError.RorConfigActionNotFound + case _ => + NodeError.Unknown(failedNodeException.getDetailedMessage) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataActionType.scala new file mode 100644 index 0000000000..af3c3bb353 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataActionType.scala @@ -0,0 +1,32 @@ +/* + * 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.actions.rrmetadata + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable +import tech.beshu.ror.accesscontrol.domain.Action.RorAction + +class RRUserMetadataActionType extends ActionType[RRUserMetadataResponse](RRUserMetadataActionType.name) + +object RRUserMetadataActionType { + val name: String = RorAction.RorUserMetadataAction.value + val instance = new RRUserMetadataActionType() + + final case object RRUserMetadataActionCannotBeTransported extends Exception + + private [rrmetadata] def exceptionReader[A]: Writeable.Reader[A] = _ => throw RRUserMetadataActionCannotBeTransported +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataRequest.scala new file mode 100644 index 0000000000..ce9cdf3cf5 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataRequest.scala @@ -0,0 +1,24 @@ +/* + * 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.actions.rrmetadata + +import org.elasticsearch.action.{ActionRequest, ActionRequestValidationException} +import tech.beshu.ror.es.actions.RorActionRequest + +class RRUserMetadataRequest extends ActionRequest with RorActionRequest { + override def validate(): ActionRequestValidationException = null +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataResponse.scala new file mode 100644 index 0000000000..e150df9993 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/RRUserMetadataResponse.scala @@ -0,0 +1,28 @@ +/* + * 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.actions.rrmetadata + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput +import org.elasticsearch.xcontent.{ToXContent, ToXContentObject, XContentBuilder} + +class RRUserMetadataResponse extends ActionResponse with ToXContentObject { + + override def writeTo(out: StreamOutput): Unit = () + + override def toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder = builder +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/TransportRRUserMetadataAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/TransportRRUserMetadataAction.scala new file mode 100644 index 0000000000..db0513db7d --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/TransportRRUserMetadataAction.scala @@ -0,0 +1,46 @@ +/* + * 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.actions.rrmetadata + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.support.{ActionFilters, HandledTransportAction} +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService + +import java.util.concurrent.Executor +import scala.annotation.nowarn + +class TransportRRUserMetadataAction(transportService: TransportService, + actionFilters: ActionFilters, + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends HandledTransportAction[RRUserMetadataRequest, RRUserMetadataResponse]( + RRUserMetadataActionType.name, transportService, actionFilters, RRUserMetadataActionType.exceptionReader[RRUserMetadataRequest], executor + ) { + + @Inject + def this(transportService: TransportService, + actionFilters: ActionFilters, + threadPool: ThreadPool) = + this(transportService, actionFilters, threadPool.executor(ThreadPool.Names.GENERIC), ()) + + override def doExecute(task: Task, request: RRUserMetadataRequest, listener: ActionListener[RRUserMetadataResponse]): Unit = { + // nothing to do here + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/rest/RestRRUserMetadataAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/rest/RestRRUserMetadataAction.scala new file mode 100644 index 0000000000..459926e709 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrmetadata/rest/RestRRUserMetadataAction.scala @@ -0,0 +1,46 @@ +/* + * 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.actions.rrmetadata.rest + +import java.util + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.rest.BaseRestHandler.RestChannelConsumer +import org.elasticsearch.rest.RestHandler.Route +import org.elasticsearch.rest.RestRequest.Method.GET +import org.elasticsearch.rest.action.RestToXContentListener +import org.elasticsearch.rest.{BaseRestHandler, RestChannel, RestHandler, RestRequest} +import tech.beshu.ror.constants +import tech.beshu.ror.es.actions.rrmetadata.{RRUserMetadataActionType, RRUserMetadataRequest, RRUserMetadataResponse} + +import scala.jdk.CollectionConverters._ + +@Inject +class RestRRUserMetadataAction() + extends BaseRestHandler with RestHandler { + + override def routes(): util.List[Route] = List( + new Route(GET, constants.CURRENT_USER_METADATA_PATH) + ).asJava + + override val getName: String = "ror-user-metadata-handler" + + override def prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer = (channel: RestChannel) => { + client.execute(new RRUserMetadataActionType, new RRUserMetadataRequest, new RestToXContentListener[RRUserMetadataResponse](channel)) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigActionHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigActionHandler.scala new file mode 100644 index 0000000000..75f7612431 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigActionHandler.scala @@ -0,0 +1,60 @@ +/* + * 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.actions.rrtestconfig + +import cats.implicits.toShow +import monix.execution.Scheduler +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.ActionListener +import tech.beshu.ror.RequestId +import tech.beshu.ror.api.TestConfigApi.TestConfigResponse +import tech.beshu.ror.boot.RorSchedulers +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged +import tech.beshu.ror.utils.RorInstanceSupplier + +class RRTestConfigActionHandler() extends Logging { + + private implicit val rorRestApiScheduler: Scheduler = RorSchedulers.restApiScheduler + + def handle(request: RRTestConfigRequest, listener: ActionListener[RRTestConfigResponse]): Unit = { + getApi match { + case Some(api) => doPrivileged { + implicit val requestId: RequestId = request.requestContextId + api + .call(request.getTestConfigRequest) + .runAsync { result => + handle(result, listener) + } + } + case None => + listener.onFailure(new Exception("TestConfig API is not available")) + } + } + + private def handle(result: Either[Throwable, TestConfigResponse], + listener: ActionListener[RRTestConfigResponse]) + (implicit requestId: RequestId): Unit = result match { + case Right(response) => + listener.onResponse(new RRTestConfigResponse(response)) + case Left(ex) => + logger.error(s"[${requestId.show}] RRTestConfig internal error", ex) + listener.onFailure(new Exception(ex)) + } + + private def getApi = + RorInstanceSupplier.get().map(_.testConfigApi) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigActionType.scala new file mode 100644 index 0000000000..4633b0d92a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigActionType.scala @@ -0,0 +1,33 @@ +/* + * 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.actions.rrtestconfig + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable +import tech.beshu.ror.accesscontrol.domain.Action.RorAction + +class RRTestConfigActionType extends ActionType[RRTestConfigResponse](RRTestConfigActionType.name) + +object RRTestConfigActionType { + val name: String = RorAction.RorTestConfigAction.value + val instance = new RRTestConfigActionType() + + final case object RRTestConfigActionCannotBeTransported extends Exception + + private [rrtestconfig] def exceptionReader[A]: Writeable.Reader[A] = _ => throw RRTestConfigActionCannotBeTransported +} + diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigRequest.scala new file mode 100644 index 0000000000..e42588f267 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigRequest.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.actions.rrtestconfig + +import org.elasticsearch.action.{ActionRequest, ActionRequestValidationException} +import org.elasticsearch.rest.RestRequest +import org.elasticsearch.rest.RestRequest.Method.{DELETE, GET, POST} +import tech.beshu.ror.api.{RorApiRequest, TestConfigApi} +import tech.beshu.ror.es.actions.RorActionRequest +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.{constants, RequestId} + +class RRTestConfigRequest(testConfigApiRequest: TestConfigApi.TestConfigRequest, + esRestRequest: RestRequest) extends ActionRequest with RorActionRequest { + + def getTestConfigRequest: RorApiRequest[TestConfigApi.TestConfigRequest] = + RorApiRequest(testConfigApiRequest, loggerUser) + + lazy val requestContextId: RequestId = RequestId(s"${esRestRequest.hashCode()}-${this.hashCode()}") + + override def validate(): ActionRequestValidationException = null +} + +object RRTestConfigRequest { + + def createFrom(request: RestRequest): RRTestConfigRequest = { + val requestType = (request.uri().addTrailingSlashIfNotPresent(), request.method()) match { + case (constants.PROVIDE_TEST_CONFIG_PATH, GET) => + TestConfigApi.TestConfigRequest.Type.ProvideTestConfig + case (constants.DELETE_TEST_CONFIG_PATH, DELETE) => + TestConfigApi.TestConfigRequest.Type.InvalidateTestConfig + case (constants.UPDATE_TEST_CONFIG_PATH, POST) => + TestConfigApi.TestConfigRequest.Type.UpdateTestConfig + case (constants.PROVIDE_LOCAL_USERS_PATH, GET) => + TestConfigApi.TestConfigRequest.Type.ProvideLocalUsers + case (unknownUri, unknownMethod) => + throw new IllegalStateException(s"Unknown request: $unknownMethod $unknownUri") + } + new RRTestConfigRequest( + TestConfigApi.TestConfigRequest( + requestType, + request.content.utf8ToString + ), + request + ) + } +} + diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigResponse.scala new file mode 100644 index 0000000000..ae59fbf01c --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/RRTestConfigResponse.scala @@ -0,0 +1,130 @@ +/* + * 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.actions.rrtestconfig + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput +import tech.beshu.ror.es.utils.StatusToXContentObject +import org.elasticsearch.rest.RestStatus +import org.elasticsearch.xcontent.{ToXContent, XContentBuilder} +import tech.beshu.ror.api.TestConfigApi +import tech.beshu.ror.api.TestConfigApi.TestConfigResponse._ + +import java.time.ZoneOffset + +class RRTestConfigResponse(response: TestConfigApi.TestConfigResponse) + extends ActionResponse with StatusToXContentObject { + + override def toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder = { + response match { + case provideConfigResponse: ProvideTestConfig => provideConfigResponse match { + case res: ProvideTestConfig.CurrentTestSettings => currentConfigJson(builder, res) + case ProvideTestConfig.TestSettingsNotConfigured(message) => addResponseJson(builder, response.status, message) + case res: ProvideTestConfig.TestSettingsInvalidated => invalidatedConfigJson(builder, res) + } + case updateConfigResponse: UpdateTestConfig => updateConfigResponse match { + case res: UpdateTestConfig.SuccessResponse => updateConfigSuccessResponseJson(builder, res) + case UpdateTestConfig.FailedResponse(message) => addResponseJson(builder, response.status, message) + } + case invalidateConfigResponse: InvalidateTestConfig => invalidateConfigResponse match { + case InvalidateTestConfig.SuccessResponse(message) => addResponseJson(builder, response.status, message) + case InvalidateTestConfig.FailedResponse(message) => addResponseJson(builder, response.status, message) + } + case provideUsersResponse: ProvideLocalUsers => provideUsersResponse match { + case res: ProvideLocalUsers.SuccessResponse => provideLocalUsersJson(builder, res) + case ProvideLocalUsers.TestSettingsNotConfigured(message) => addResponseJson(builder, response.status, message) + case ProvideLocalUsers.TestSettingsInvalidated(message) => addResponseJson(builder, response.status, message) + } + case failure: Failure => failure match { + case Failure.BadRequest(message) => addResponseJson(builder, response.status, message) + } + } + builder + } + + override def writeTo(out: StreamOutput): Unit = () + + override def status: RestStatus = response match { + case _: ProvideTestConfig => RestStatus.OK + case _: UpdateTestConfig => RestStatus.OK + case _: InvalidateTestConfig => RestStatus.OK + case _: ProvideLocalUsers => RestStatus.OK + case failure: Failure => failure match { + case Failure.BadRequest(_) => RestStatus.BAD_REQUEST + } + } + + private def addResponseJson(builder: XContentBuilder, status: String, message: String): Unit = { + builder.startObject + builder.field("status", status) + builder.field("message", message) + builder.endObject + } + + private def currentConfigJson(builder: XContentBuilder, response: ProvideTestConfig.CurrentTestSettings): Unit = { + builder.startObject + builder.field("status", response.status) + builder.field("ttl", response.ttl.toString()) + builder.field("settings", response.settings.raw) + builder.field("valid_to", response.validTo.atOffset(ZoneOffset.UTC).toString) + warningsJson(builder, response.warnings) + builder.endObject + } + + private def invalidatedConfigJson(builder: XContentBuilder, response: ProvideTestConfig.TestSettingsInvalidated): Unit = { + builder.startObject + builder.field("status", response.status) + builder.field("message", response.message) + builder.field("settings", response.settings.raw) + builder.field("ttl", response.ttl.toString()) + builder.endObject + } + + private def updateConfigSuccessResponseJson(builder: XContentBuilder, response: UpdateTestConfig.SuccessResponse): Unit = { + builder.startObject + builder.field("status", response.status) + builder.field("message", response.message) + builder.field("valid_to", response.validTo.atOffset(ZoneOffset.UTC).toString) + warningsJson(builder, response.warnings) + builder.endObject + } + + private def provideLocalUsersJson(builder: XContentBuilder, response: ProvideLocalUsers.SuccessResponse): Unit = { + builder.startObject + builder.field("status", response.status) + builder.startArray("users") + response.users.foreach { user => + builder.value(user) + } + builder.endArray() + builder.field("unknown_users", response.unknownUsers) + builder.endObject + } + + private def warningsJson(builder: XContentBuilder, warnings: List[Warning]): Unit = { + builder.startArray("warnings") + warnings.foreach { warning => + builder.startObject() + builder.field("block_name", warning.blockName) + builder.field("rule_name", warning.ruleName) + builder.field("message", warning.message) + builder.field("hint", warning.hint) + builder.endObject() + } + builder.endArray() + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/TransportRRTestConfigAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/TransportRRTestConfigAction.scala new file mode 100644 index 0000000000..95217a5dfa --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/TransportRRTestConfigAction.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.es.actions.rrtestconfig + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.support.{ActionFilters, HandledTransportAction} +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService + +import java.util.concurrent.Executor +import scala.annotation.nowarn + +class TransportRRTestConfigAction(transportService: TransportService, + actionFilters: ActionFilters, + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends HandledTransportAction[RRTestConfigRequest, RRTestConfigResponse]( + RRTestConfigActionType.name, transportService, actionFilters, RRTestConfigActionType.exceptionReader[RRTestConfigRequest], executor + ) { + + @Inject + def this(transportService: TransportService, + actionFilters: ActionFilters, + threadPool: ThreadPool) = + this(transportService, actionFilters, threadPool.executor(ThreadPool.Names.GENERIC), ()) + + private val handler = new RRTestConfigActionHandler() + + override def doExecute(task: Task, request: RRTestConfigRequest, listener: ActionListener[RRTestConfigResponse]): Unit = { + handler.handle(request, listener) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/rest/RestRRTestConfigAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/rest/RestRRTestConfigAction.scala new file mode 100644 index 0000000000..4c56e50e37 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/rrtestconfig/rest/RestRRTestConfigAction.scala @@ -0,0 +1,52 @@ +/* + * 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.actions.rrtestconfig.rest + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.rest.BaseRestHandler.RestChannelConsumer +import org.elasticsearch.rest.RestHandler.Route +import org.elasticsearch.rest.RestRequest.Method.{DELETE, GET, POST} +import org.elasticsearch.rest._ +import tech.beshu.ror.constants +import tech.beshu.ror.es.actions.rrtestconfig.{RRTestConfigActionType, RRTestConfigRequest, RRTestConfigResponse} +import tech.beshu.ror.es.utils.RestToXContentWithStatusListener + +import java.util +import scala.jdk.CollectionConverters._ + +@Inject +class RestRRTestConfigAction() + extends BaseRestHandler with RestHandler { + + override def routes(): util.List[Route] = List( + new Route(GET, constants.PROVIDE_TEST_CONFIG_PATH), + new Route(POST, constants.UPDATE_TEST_CONFIG_PATH), + new Route(DELETE, constants.DELETE_TEST_CONFIG_PATH), + new Route(GET, constants.PROVIDE_LOCAL_USERS_PATH) + ).asJava + + override val getName: String = "ror-test-config-handler" + + override def prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer = new RestChannelConsumer { + private val rorTestConfigRequest = RRTestConfigRequest.createFrom(request) + + override def accept(channel: RestChannel): Unit = { + client.execute(new RRTestConfigActionType, rorTestConfigRequest, new RestToXContentWithStatusListener[RRTestConfigResponse](channel)) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatActionType.scala new file mode 100644 index 0000000000..7ea3756a42 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatActionType.scala @@ -0,0 +1,31 @@ +/* + * 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.actions.wrappers._cat + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable + +class RorWrappedCatActionType extends ActionType[RorWrappedCatResponse](RorWrappedCatActionType.name) + +object RorWrappedCatActionType { + val name = "cat_action" + val instance = new RorWrappedCatActionType() + + final case object ArtificialCatActionCannotBeTransported extends Exception + + private [_cat] def exceptionReader[A]: Writeable.Reader[A] = _ => throw ArtificialCatActionCannotBeTransported +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatRequest.scala new file mode 100644 index 0000000000..39b42d8fae --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatRequest.scala @@ -0,0 +1,24 @@ +/* + * 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.actions.wrappers._cat + +import org.elasticsearch.action.{ActionRequest, ActionRequestValidationException} + +class RorWrappedCatRequest(val sendResponse: () => Unit) + extends ActionRequest { + override def validate(): ActionRequestValidationException = null +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatResponse.scala new file mode 100644 index 0000000000..540435f440 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/RorWrappedCatResponse.scala @@ -0,0 +1,24 @@ +/* + * 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.actions.wrappers._cat + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput + +class RorWrappedCatResponse extends ActionResponse { + override def writeTo(out: StreamOutput): Unit = () +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/TransportRorWrappedCatAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/TransportRorWrappedCatAction.scala new file mode 100644 index 0000000000..775fbba485 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/TransportRorWrappedCatAction.scala @@ -0,0 +1,47 @@ +/* + * 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.actions.wrappers._cat + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.support.{ActionFilters, HandledTransportAction} +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService + +import java.util.concurrent.Executor +import scala.annotation.nowarn + +class TransportRorWrappedCatAction(transportService: TransportService, + actionFilters: ActionFilters, + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends HandledTransportAction[RorWrappedCatRequest, RorWrappedCatResponse]( + RorWrappedCatActionType.name, transportService, actionFilters, RorWrappedCatActionType.exceptionReader[RorWrappedCatRequest], executor + ) { + + @Inject + def this(transportService: TransportService, + actionFilters: ActionFilters, + threadPool: ThreadPool) = + this(transportService, actionFilters, threadPool.executor(ThreadPool.Names.GENERIC), ()) + + override def doExecute(task: Task, request: RorWrappedCatRequest, + listener: ActionListener[RorWrappedCatResponse]): Unit = { + listener.onResponse(new RorWrappedCatResponse) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/rest/RorWrappedRestCatAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/rest/RorWrappedRestCatAction.scala new file mode 100644 index 0000000000..d84b113f21 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_cat/rest/RorWrappedRestCatAction.scala @@ -0,0 +1,45 @@ +/* + * 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.actions.wrappers._cat.rest + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.rest.action.RestActionListener +import org.elasticsearch.rest.action.cat.RestCatAction +import org.elasticsearch.rest.{BaseRestHandler, RestChannel, RestHandler, RestRequest} +import tech.beshu.ror.es.actions.wrappers._cat.{RorWrappedCatActionType, RorWrappedCatRequest, RorWrappedCatResponse} + +import java.util + +class RorWrappedRestCatAction(catAction: RestCatAction) extends BaseRestHandler { + + override val getName: String = catAction.getName + + override def routes(): util.List[RestHandler.Route] = catAction.routes() + + override def prepareRequest(request: RestRequest, client: NodeClient): BaseRestHandler.RestChannelConsumer = { + (channel: RestChannel) => + def sendResponse(): Unit = catAction.prepareRequest(request, client).accept(channel) + + client.execute( + RorWrappedCatActionType.instance, + new RorWrappedCatRequest(sendResponse), + new RestActionListener[RorWrappedCatResponse](channel) { + override def processResponse(response: RorWrappedCatResponse): Unit = sendResponse() + } + ) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeActionType.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeActionType.scala new file mode 100644 index 0000000000..0c607ae4f7 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeActionType.scala @@ -0,0 +1,32 @@ +/* + * 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.actions.wrappers._upgrade + +import org.elasticsearch.action.ActionType +import org.elasticsearch.common.io.stream.Writeable +import org.elasticsearch.rest.action.admin.indices.RestUpgradeActionDeprecated + +class RorWrappedUpgradeActionType extends ActionType[RorWrappedUpgradeResponse](RorWrappedUpgradeActionType.name) + +object RorWrappedUpgradeActionType { + val name = new RestUpgradeActionDeprecated().getName + val instance = new RorWrappedUpgradeActionType() + + final case object ArtificialUpgradeActionCannotBeTransported extends Exception + + private [_upgrade] def exceptionReader[A]: Writeable.Reader[A] = _ => throw ArtificialUpgradeActionCannotBeTransported +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeRequest.scala new file mode 100644 index 0000000000..b59d7c013c --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeRequest.scala @@ -0,0 +1,24 @@ +/* + * 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.actions.wrappers._upgrade + +import org.elasticsearch.action.{ActionRequest, ActionRequestValidationException} + +class RorWrappedUpgradeRequest(val sendResponse: () => Unit) + extends ActionRequest { + override def validate(): ActionRequestValidationException = null +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeResponse.scala new file mode 100644 index 0000000000..975f4c27cd --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/RorWrappedUpgradeResponse.scala @@ -0,0 +1,24 @@ +/* + * 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.actions.wrappers._upgrade + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput + +class RorWrappedUpgradeResponse extends ActionResponse { + override def writeTo(out: StreamOutput): Unit = () +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/TransportRorWrappedUpgradeAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/TransportRorWrappedUpgradeAction.scala new file mode 100644 index 0000000000..0efbd48a97 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/TransportRorWrappedUpgradeAction.scala @@ -0,0 +1,47 @@ +/* + * 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.actions.wrappers._upgrade + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.support.{ActionFilters, HandledTransportAction} +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.tasks.Task +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.TransportService + +import java.util.concurrent.Executor +import scala.annotation.nowarn + +class TransportRorWrappedUpgradeAction(transportService: TransportService, + actionFilters: ActionFilters, + executor: Executor, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends HandledTransportAction[RorWrappedUpgradeRequest, RorWrappedUpgradeResponse]( + RorWrappedUpgradeActionType.name, transportService, actionFilters, RorWrappedUpgradeActionType.exceptionReader[RorWrappedUpgradeRequest], executor + ) { + + @Inject + def this(transportService: TransportService, + actionFilters: ActionFilters, + threadPool: ThreadPool) = + this(transportService, actionFilters, threadPool.executor(ThreadPool.Names.GENERIC), ()) + + override def doExecute(task: Task, request: RorWrappedUpgradeRequest, + listener: ActionListener[RorWrappedUpgradeResponse]): Unit = { + listener.onResponse(new RorWrappedUpgradeResponse) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/rest/RorWrappedRestUpgradeAction.scala b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/rest/RorWrappedRestUpgradeAction.scala new file mode 100644 index 0000000000..3c1be8015a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/actions/wrappers/_upgrade/rest/RorWrappedRestUpgradeAction.scala @@ -0,0 +1,45 @@ +/* + * 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.actions.wrappers._upgrade.rest + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.rest.action.RestActionListener +import org.elasticsearch.rest.action.admin.indices.RestUpgradeActionDeprecated +import org.elasticsearch.rest.{BaseRestHandler, RestChannel, RestHandler, RestRequest} +import tech.beshu.ror.es.actions.wrappers._upgrade.{RorWrappedUpgradeActionType, RorWrappedUpgradeRequest, RorWrappedUpgradeResponse} + +import java.util + +class RorWrappedRestUpgradeAction(upgradeAction: RestUpgradeActionDeprecated) extends BaseRestHandler { + + override val getName: String = upgradeAction.getName + + override def routes(): util.List[RestHandler.Route] = upgradeAction.routes() + + override def prepareRequest(request: RestRequest, client: NodeClient): BaseRestHandler.RestChannelConsumer = { + (channel: RestChannel) => + def sendResponse(): Unit = upgradeAction.prepareRequest(request, client).accept(channel) + + client.execute( + RorWrappedUpgradeActionType.instance, + new RorWrappedUpgradeRequest(sendResponse), + new RestActionListener[RorWrappedUpgradeResponse](channel) { + override def processResponse(response: RorWrappedUpgradeResponse): Unit = sendResponse() + } + ) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/RoleIndexSearcherWrapper.scala b/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/RoleIndexSearcherWrapper.scala new file mode 100644 index 0000000000..0ea53e2969 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/RoleIndexSearcherWrapper.scala @@ -0,0 +1,81 @@ +/* + * 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.dlsfls + +import cats.data.StateT +import cats.implicits._ +import eu.timepit.refined.types.string.NonEmptyString +import org.apache.logging.log4j.scala.Logging +import org.apache.lucene.index.DirectoryReader +import org.elasticsearch.common.util.concurrent.ThreadContext +import org.elasticsearch.core.CheckedFunction +import org.elasticsearch.index.IndexService +import tech.beshu.ror.constants +import tech.beshu.ror.accesscontrol.headerValues.transientFieldsFromHeaderValue + +import java.io.IOException +import java.util.function.{Function => JavaFunction} +import scala.util.{Failure, Success, Try} + +object RoleIndexSearcherWrapper extends Logging { + + val instance: JavaFunction[IndexService, CheckedFunction[DirectoryReader, DirectoryReader, IOException]] = + new JavaFunction[IndexService, CheckedFunction[DirectoryReader, DirectoryReader, IOException]] { + + override def apply(indexService: IndexService): CheckedFunction[DirectoryReader, DirectoryReader, IOException] = { + val threadContext: ThreadContext = indexService.getThreadPool.getThreadContext + reader: DirectoryReader => + prepareDocumentFieldReader(threadContext) + .run(reader).get._2 + } + + private def prepareDocumentFieldReader(threadContext: ThreadContext): StateT[Try, DirectoryReader, DirectoryReader] = { + StateT { reader => + Option(threadContext.getHeader(constants.FIELDS_TRANSIENT)) match { + case Some(fieldsHeader) => + fieldsFromHeaderValue(fieldsHeader) + .flatMap { fields => + Try(RorDocumentFieldReader.wrap(reader, fields)) + .recover { case e => throw new IllegalStateException("FLS: Couldn't extract FLS fields from threadContext", e) } + } + .map(r => (r, r)) + case None => + logger.debug(s"FLS: ${constants.FIELDS_TRANSIENT} not found in threadContext") + Success((reader, reader)) + } + } + } + + private def fieldsFromHeaderValue(value: String) = { + lazy val failure = Failure(new IllegalStateException("FLS: Couldn't extract FLS fields from threadContext")) + for { + nel <- NonEmptyString.from(value) match { + case Right(nel) => Success(nel) + case Left(_) => + logger.debug("FLS: empty header value") + failure + } + fields <- transientFieldsFromHeaderValue.fromRawValue(nel) match { + case result@Success(_) => result + case Failure(ex) => + logger.debug(s"FLS: Cannot decode fields from ${constants.FIELDS_TRANSIENT} header value", ex) + failure + } + } yield fields + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/RorDocumentFieldReader.scala b/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/RorDocumentFieldReader.scala new file mode 100644 index 0000000000..6764a999b7 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/RorDocumentFieldReader.scala @@ -0,0 +1,206 @@ +/* + * 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.dlsfls + +import com.google.common.collect.Iterators +import org.apache.logging.log4j.scala.Logging +import org.apache.lucene.codecs.StoredFieldsReader +import org.apache.lucene.index.StoredFieldVisitor.Status +import org.apache.lucene.index._ +import org.apache.lucene.util.Bits +import org.elasticsearch.ExceptionsHelper +import org.elasticsearch.common.bytes.{BytesArray, BytesReference} +import org.elasticsearch.common.lucene.index.SequentialStoredFieldsLeafReader +import org.elasticsearch.common.xcontent.XContentHelper +import org.elasticsearch.xcontent.{XContentBuilder, XContentType} +import tech.beshu.ror.constants +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions +import tech.beshu.ror.es.dlsfls.RorDocumentFieldDirectoryReader.RorDocumentFieldDirectorySubReader +import tech.beshu.ror.es.utils.XContentBuilderOps._ +import tech.beshu.ror.fls.{FieldsPolicy, JsonPolicyBasedFilterer} + +import java.io.ByteArrayOutputStream +import java.util.{Iterator => JavaIterator} +import scala.jdk.CollectionConverters._ +import scala.util.Try + +private class RorDocumentFieldReader(reader: LeafReader, fieldsRestrictions: FieldsRestrictions) + extends SequentialStoredFieldsLeafReader(reader) with Logging { + + private val policy = new FieldsPolicy(fieldsRestrictions) + private val remainingFieldsInfo = { + val fInfos = in.getFieldInfos + val newInfos = if (fInfos.asScala.isEmpty) { + logger.warn("original fields were empty! This is weird!") + fInfos + } else { + val remainingFields = fInfos.asScala.filter(f => policy.canKeep(f.name)).toSet + new FieldInfos(remainingFields.toArray) + } + logger.debug(s"always allow: ${constants.FIELDS_ALWAYS_ALLOW.mkString(",")}") + logger.debug(s"original fields were: ${fInfos.asScala.map(_.name).mkString(",")}") + logger.debug(s"new fields are: ${newInfos.asScala.map(_.name).mkString(",")}") + newInfos + } + private val jsonPolicyBasedFilterer = new JsonPolicyBasedFilterer(policy) + + override def getFieldInfos: FieldInfos = remainingFieldsInfo + + override def termVectors(): TermVectors = { + val originalTermVectors = in.termVectors() + new TermVectors { + override def get(doc: Int): Fields = new Fields { + private val originalFields = originalTermVectors.get(doc) + + override def iterator(): JavaIterator[String] = Iterators.filter(originalFields.iterator, (s: String) => policy.canKeep(s)) + override def terms(field: String): Terms = if (policy.canKeep(field)) originalFields.terms(field) else null + override def size(): Int = remainingFieldsInfo.size + } + } + } + + override def getNumericDocValues(field: String): NumericDocValues = + if (policy.canKeep(field)) in.getNumericDocValues(field) else null + + override def getBinaryDocValues(field: String): BinaryDocValues = + if (policy.canKeep(field)) in.getBinaryDocValues(field) else null + + override def getNormValues(field: String): NumericDocValues = + if (policy.canKeep(field)) in.getNormValues(field) else null + + override def getSortedDocValues(field: String): SortedDocValues = + if (policy.canKeep(field)) in.getSortedDocValues(field) else null + + override def getSortedNumericDocValues(field: String): SortedNumericDocValues = + if (policy.canKeep(field)) in.getSortedNumericDocValues(field) else null + + override def getSortedSetDocValues(field: String): SortedSetDocValues = + if (policy.canKeep(field)) in.getSortedSetDocValues(field) else null + + override def getPointValues(field: String): PointValues = + if (policy.canKeep(field)) in.getPointValues(field) else null + + override def terms(field: String): Terms = + if (policy.canKeep(field)) in.terms(field) else null + + override def getMetaData: LeafMetaData = in.getMetaData + + override def getLiveDocs: Bits = in.getLiveDocs + + override def numDocs: Int = in.numDocs + + override def getDelegate: LeafReader = in + + override def document(docID: Int, visitor: StoredFieldVisitor): Unit = + super.document(docID, new RorStoredFieldVisitorDecorator(visitor)) + + override def storedFields(): StoredFields = { + val storedFields = super.storedFields() + new StoredFields { + override def document(docID: Int, visitor: StoredFieldVisitor): Unit = + storedFields.document(docID, new RorStoredFieldVisitorDecorator(visitor)) + } + } + + override def getCoreCacheHelper: IndexReader.CacheHelper = this.in.getCoreCacheHelper + + override def getReaderCacheHelper: IndexReader.CacheHelper = this.in.getCoreCacheHelper + + override def doGetSequentialStoredFieldsReader(reader: StoredFieldsReader): StoredFieldsReader = + new RorStoredFieldsReaderDecorator(reader) + + private class RorStoredFieldsReaderDecorator(final val underlying: StoredFieldsReader) + extends StoredFieldsReaderForScalaHelper(underlying) { + + override def document(docID: Int, visitor: StoredFieldVisitor): Unit = { + underlying.document(docID, new RorStoredFieldVisitorDecorator(visitor)) + } + + override def clone(): StoredFieldsReader = { + new RorStoredFieldsReaderDecorator(this.cloneUnderlying()) + } + } + + private class RorStoredFieldVisitorDecorator(underlying: StoredFieldVisitor) + extends StoredFieldVisitor { + + override def needsField(fieldInfo: FieldInfo): StoredFieldVisitor.Status = + if (policy.canKeep(fieldInfo.name)) underlying.needsField(fieldInfo) else Status.NO + + override def hashCode: Int = underlying.hashCode + + override def stringField(fieldInfo: FieldInfo, value: String): Unit = underlying.stringField(fieldInfo, value) + + override def equals(obj: Any): Boolean = underlying == obj + + override def doubleField(fieldInfo: FieldInfo, value: Double): Unit = underlying.doubleField(fieldInfo, value) + + override def floatField(fieldInfo: FieldInfo, value: Float): Unit = underlying.floatField(fieldInfo, value) + + override def intField(fieldInfo: FieldInfo, value: Int): Unit = underlying.intField(fieldInfo, value) + + override def longField(fieldInfo: FieldInfo, value: Long): Unit = underlying.longField(fieldInfo, value) + + override def binaryField(fieldInfo: FieldInfo, value: Array[Byte]): Unit = { + if ("_source" != fieldInfo.name) { + underlying.binaryField(fieldInfo, value) + } else { + val filteredJson = jsonPolicyBasedFilterer.filteredJson(ujson.read(value)) + + val xBuilder = XContentBuilder + .builder( + XContentHelper + .convertToMap(new BytesArray(value), false, XContentType.JSON) + .v1().xContent() + ) + .json(filteredJson) + + val out = new ByteArrayOutputStream + BytesReference.bytes(xBuilder).writeTo(out) + underlying.binaryField(fieldInfo, out.toByteArray) + } + } + } +} + +object RorDocumentFieldReader { + + def wrap(in: DirectoryReader, fieldsRestrictions: FieldsRestrictions): RorDocumentFieldDirectoryReader = + new RorDocumentFieldDirectoryReader(in, fieldsRestrictions) +} + +final class RorDocumentFieldDirectoryReader(in: DirectoryReader, fieldsRestrictions: FieldsRestrictions) + extends FilterDirectoryReader(in, new RorDocumentFieldDirectorySubReader(fieldsRestrictions)) { + + override protected def doWrapDirectoryReader(in: DirectoryReader) = + new RorDocumentFieldDirectoryReader(in, fieldsRestrictions) + + override def getReaderCacheHelper: IndexReader.CacheHelper = + in.getReaderCacheHelper +} + +object RorDocumentFieldDirectoryReader { + private class RorDocumentFieldDirectorySubReader(fieldsRestrictions: FieldsRestrictions) + extends FilterDirectoryReader.SubReaderWrapper { + + override def wrap(reader: LeafReader): LeafReader = { + Try(new RorDocumentFieldReader(reader, fieldsRestrictions)) + .recover { case ex: Exception => throw ExceptionsHelper.convertToElastic(ex) } + .get + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/StoredFieldsReaderForScalaHelper.java b/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/StoredFieldsReaderForScalaHelper.java new file mode 100644 index 0000000000..1c571da6ac --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/dlsfls/StoredFieldsReaderForScalaHelper.java @@ -0,0 +1,59 @@ +/* + * 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.dlsfls; + +import org.apache.lucene.codecs.StoredFieldsReader; +import org.apache.lucene.index.StoredFieldVisitor; + +import java.io.IOException; + +// hack: we need this class because when we try to call `clone` from Scala, the java.Object#clone is called and +// compiler tells us that protected method cannot be called. The method is overridden in the StoredFieldsReader +// class and should be visible as a public one. But it's not. That's why we need this helper. +public class StoredFieldsReaderForScalaHelper extends StoredFieldsReader { + + private final StoredFieldsReader underlying; + + public StoredFieldsReaderForScalaHelper(StoredFieldsReader underlying) { + this.underlying = underlying; + } + + @Override + public void document(int docID, StoredFieldVisitor visitor) throws IOException { + underlying.document(docID, visitor); + } + + @Override + public StoredFieldsReader clone() { + return new StoredFieldsReaderForScalaHelper(cloneUnderlying()); + } + + @Override + public void checkIntegrity() throws IOException { + underlying.checkIntegrity(); + } + + @Override + public void close() throws IOException { + underlying.close(); + } + + public StoredFieldsReader cloneUnderlying() { + return underlying.clone(); + } + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala new file mode 100644 index 0000000000..58d878b94d --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/AclAwareRequestFilter.scala @@ -0,0 +1,312 @@ +/* + * 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 + +import cats.implicits._ +import monix.eval.Task +import monix.execution.Scheduler +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action._ +import org.elasticsearch.action.admin.cluster.allocation.ClusterAllocationExplainRequest +import org.elasticsearch.action.admin.cluster.repositories.cleanup.CleanupRepositoryRequest +import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest +import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRequest +import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest +import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest +import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteRequest +import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest +import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest +import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest +import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest +import org.elasticsearch.action.admin.indices.get.GetIndexRequest +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction +import org.elasticsearch.action.admin.indices.rollover.RolloverRequest +import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest +import org.elasticsearch.action.admin.indices.shards.IndicesShardStoresRequest +import org.elasticsearch.action.admin.indices.shrink.ResizeRequest +import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest +import org.elasticsearch.action.admin.indices.template.delete.{DeleteIndexTemplateRequest, TransportDeleteComponentTemplateAction, TransportDeleteComposableIndexTemplateAction} +import org.elasticsearch.action.admin.indices.template.get.{GetComponentTemplateAction, GetComposableIndexTemplateAction, GetIndexTemplatesRequest} +import org.elasticsearch.action.admin.indices.template.post.{SimulateIndexTemplateRequest, SimulateTemplateAction} +import org.elasticsearch.action.admin.indices.template.put.{PutComponentTemplateAction, PutIndexTemplateRequest, TransportPutComposableIndexTemplateAction} +import org.elasticsearch.action.bulk.{BulkRequest, BulkShardRequest} +import org.elasticsearch.action.datastreams.{CreateDataStreamAction, DataStreamsStatsAction, DeleteDataStreamAction, GetDataStreamAction, MigrateToDataStreamAction, ModifyDataStreamsAction, PromoteDataStreamAction} +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} +import org.elasticsearch.action.support.ActionFilterChain +import org.elasticsearch.action.termvectors.MultiTermVectorsRequest +import org.elasticsearch.common.settings.Settings +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.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain.{Action, Header} +import tech.beshu.ror.accesscontrol.matchers.UniqueIdentifierGenerator +import tech.beshu.ror.boot.ReadonlyRest.Engine +import tech.beshu.ror.boot.engines.Engines +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.RestRequestOps._ +import tech.beshu.ror.es.handler.request.context.types._ +import tech.beshu.ror.es.handler.request.context.types.datastreams._ +import tech.beshu.ror.es.handler.request.context.types.repositories._ +import tech.beshu.ror.es.handler.request.context.types.ror._ +import tech.beshu.ror.es.handler.request.context.types.snapshots._ +import tech.beshu.ror.es.handler.request.context.types.templates._ +import tech.beshu.ror.es.{ResponseFieldsFiltering, RorClusterService} + +import java.time.Instant +import scala.reflect.ClassTag + +class AclAwareRequestFilter(clusterService: RorClusterService, + settings: Settings, + threadPool: ThreadPool) + (implicit generator: UniqueIdentifierGenerator, + scheduler: Scheduler) + extends Logging { + + def handle(engines: Engines, + esContext: EsContext): Task[Either[Error, Unit]] = { + esContext + .pickEngineToHandle(engines) + .map(handleRequestWithEngine(_, esContext)) + .sequence + } + + private def handleRequestWithEngine(engine: Engine, + esContext: EsContext) = { + esContext.actionRequest match { + case request: RRUserMetadataRequest => + val handler = new CurrentUserMetadataRequestHandler(engine, esContext) + handler.handle(new CurrentUserMetadataEsRequestContext(request, esContext, clusterService, threadPool)) + case _ => + val regularRequestHandler = new RegularRequestHandler(engine, esContext, threadPool) + handleEsRestApiRequest(regularRequestHandler, esContext, engine.core.accessControl.staticContext) + } + } + + private def handleEsRestApiRequest(regularRequestHandler: RegularRequestHandler, + esContext: EsContext, + aclContext: AccessControlStaticContext) = { + esContext.actionRequest match { + case request: RRAuditEventRequest => + regularRequestHandler.handle(new AuditEventESRequestContext(request, esContext, clusterService, threadPool)) + case request: RorActionRequest => + regularRequestHandler.handle(new RorApiEsRequestContext(request, esContext, clusterService, threadPool)) + // snapshots + case request: GetSnapshotsRequest => + regularRequestHandler.handle(new GetSnapshotsEsRequestContext(request, esContext, clusterService, threadPool)) + case request: CreateSnapshotRequest => + regularRequestHandler.handle(new CreateSnapshotEsRequestContext(request, esContext, clusterService, threadPool)) + case request: DeleteSnapshotRequest => + regularRequestHandler.handle(new DeleteSnapshotEsRequestContext(request, esContext, clusterService, threadPool)) + case request: RestoreSnapshotRequest => + regularRequestHandler.handle(new RestoreSnapshotEsRequestContext(request, esContext, clusterService, threadPool)) + case request: SnapshotsStatusRequest => + regularRequestHandler.handle(new SnapshotsStatusEsRequestContext(request, esContext, clusterService, threadPool)) + // repositories + case request: GetRepositoriesRequest => + regularRequestHandler.handle(new GetRepositoriesEsRequestContext(request, esContext, clusterService, threadPool)) + case request: PutRepositoryRequest => + regularRequestHandler.handle(new CreateRepositoryEsRequestContext(request, esContext, clusterService, threadPool)) + case request: DeleteRepositoryRequest => + regularRequestHandler.handle(new DeleteRepositoryEsRequestContext(request, esContext, clusterService, threadPool)) + case request: VerifyRepositoryRequest => + regularRequestHandler.handle(new VerifyRepositoryEsRequestContext(request, esContext, clusterService, threadPool)) + case request: CleanupRepositoryRequest => + regularRequestHandler.handle(new CleanupRepositoryEsRequestContext(request, esContext, clusterService, threadPool)) + // templates + case request: GetIndexTemplatesRequest => + regularRequestHandler.handle(new GetTemplatesEsRequestContext(request, esContext, clusterService, threadPool)) + case request: PutIndexTemplateRequest => + regularRequestHandler.handle(new PutTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: DeleteIndexTemplateRequest => + regularRequestHandler.handle(new DeleteTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: GetComposableIndexTemplateAction.Request => + regularRequestHandler.handle(new GetComposableIndexTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: TransportPutComposableIndexTemplateAction.Request => + regularRequestHandler.handle(new PutComposableIndexTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: TransportDeleteComposableIndexTemplateAction.Request => + regularRequestHandler.handle(new DeleteComposableIndexTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: GetComponentTemplateAction.Request => + regularRequestHandler.handle(new GetComponentTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: PutComponentTemplateAction.Request => + regularRequestHandler.handle(new PutComponentTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: TransportDeleteComponentTemplateAction.Request => + regularRequestHandler.handle(new DeleteComponentTemplateEsRequestContext(request, esContext, clusterService, threadPool)) + case request: SimulateIndexTemplateRequest => + regularRequestHandler.handle(new SimulateIndexTemplateRequestEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: SimulateTemplateAction.Request => + regularRequestHandler.handle(SimulateTemplateRequestEsRequestContext.from(request, esContext, clusterService, threadPool)) + // aliases + case request: GetAliasesRequest => + regularRequestHandler.handle(new GetAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: IndicesAliasesRequest => + regularRequestHandler.handle(new IndicesAliasesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + // data streams + case request: CreateDataStreamAction.Request => + regularRequestHandler.handle(new CreateDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) + case request: DataStreamsStatsAction.Request => + regularRequestHandler.handle(new DataStreamsStatsEsRequestContext(request, esContext, clusterService, threadPool)) + case request: DeleteDataStreamAction.Request => + regularRequestHandler.handle(new DeleteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) + case request: GetDataStreamAction.Request => + regularRequestHandler.handle(new GetDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) + case request: MigrateToDataStreamAction.Request => + regularRequestHandler.handle(new MigrateToDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) + case request: ModifyDataStreamsAction.Request => + regularRequestHandler.handle(new ModifyDataStreamsEsRequestContext(request, esContext, clusterService, threadPool)) + case request: PromoteDataStreamAction.Request => + regularRequestHandler.handle(new PromoteDataStreamEsRequestContext(request, esContext, clusterService, threadPool)) + // indices + case request: GetIndexRequest => + regularRequestHandler.handle(new GetIndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: BulkShardRequest => + regularRequestHandler.handle(new BulkShardEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: IndexRequest => + regularRequestHandler.handle(new IndexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: MultiGetRequest => + regularRequestHandler.handle(new MultiGetEsRequestContext(request, esContext, clusterService, threadPool)) + case request: SearchRequest => + regularRequestHandler.handle(new SearchEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: GetRequest => + regularRequestHandler.handle(new GetEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: MultiSearchRequest => + regularRequestHandler.handle(new MultiSearchEsRequestContext(request, esContext, clusterService, threadPool)) + case request: MultiTermVectorsRequest => + regularRequestHandler.handle(new MultiTermVectorsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: BulkRequest => + regularRequestHandler.handle(new BulkEsRequestContext(request, esContext, clusterService, threadPool)) + case request: DeleteRequest => + regularRequestHandler.handle(new DeleteDocumentEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: GetSettingsRequest => + regularRequestHandler.handle(new GetSettingsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: IndicesStatsRequest => + regularRequestHandler.handle(new IndicesStatsEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: IndicesShardStoresRequest => + regularRequestHandler.handle(new IndicesShardStoresEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: ClusterStateRequest => + TemplateClusterStateEsRequestContext.from(request, esContext, clusterService, settings, threadPool) match { + case Some(requestContext) => + regularRequestHandler.handle(requestContext) + case None => + regularRequestHandler.handle(new ClusterStateEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + } + case request: ClusterAllocationExplainRequest => + regularRequestHandler.handle(new ClusterAllocationExplainEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: RolloverRequest => + 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 => + regularRequestHandler.handle(new IndicesReplaceableEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: ReindexRequest => + regularRequestHandler.handle(new ReindexEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: ResizeRequest => + regularRequestHandler.handle(new ResizeEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: ClusterRerouteRequest => + regularRequestHandler.handle(new ClusterRerouteEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + case request: CompositeIndicesRequest => + ReflectionBasedActionRequest(esContext, aclContext, clusterService, threadPool) match { + case SqlIndicesEsRequestContext(r) => regularRequestHandler.handle(r) + case SearchTemplateEsRequestContext(r) => regularRequestHandler.handle(r) + case MultiSearchTemplateEsRequestContext(r) => regularRequestHandler.handle(r) + case _ => + logger.error(s"Found an child request of CompositeIndicesRequest that could not be handled: report this as a bug immediately! ${request.getClass.getSimpleName}") + regularRequestHandler.handle(new DummyCompositeIndicesEsRequestContext(request, esContext, aclContext, clusterService, threadPool)) + } + // rest + case _ => + ReflectionBasedActionRequest(esContext, aclContext, clusterService, threadPool) match { + case XpackAsyncSearchRequestContext(request) => regularRequestHandler.handle(request) + // rollup + case PutRollupJobEsRequestContext(request) => regularRequestHandler.handle(request) + case GetRollupCapsEsRequestContext(request) => regularRequestHandler.handle(request) + // indices based + case ReflectionBasedIndicesEsRequestContext(request) => regularRequestHandler.handle(request) + // rest + case _ => + regularRequestHandler.handle { + new GeneralNonIndexEsRequestContext(esContext.actionRequest, esContext, clusterService, threadPool) + } + } + } + } + +} + +object AclAwareRequestFilter { + final case class EsContext(channel: RestChannel with ResponseFieldsFiltering, + nodeName: String, + task: EsTask, + action: Action, + actionRequest: ActionRequest, + listener: ActionListener[ActionResponse], + chain: EsChain, + threadContextResponseHeaders: Set[(String, String)]) { + lazy val requestContextId = s"${channel.request().hashCode()}-${actionRequest.hashCode()}#${task.getId}" + val timestamp: Instant = Instant.now() + + def pickEngineToHandle(engines: Engines): Either[Error, Engine] = { + val impersonationHeaderPresent = isImpersonationHeader + engines.impersonatorsEngine match { + case Some(impersonatorsEngine) if impersonationHeaderPresent => Right(impersonatorsEngine) + case None if impersonationHeaderPresent => Left(Error.ImpersonatorsEngineNotConfigured) + case Some(_) | None => Right(engines.mainEngine) + } + } + + private def isImpersonationHeader = { + channel + .request() + .allHeaders() + .exists { case Header(name, _) => name === Header.Name.impersonateAs } + } + } + + final class EsChain(chain: ActionFilterChain[ActionRequest, ActionResponse]) { + + def continue(esContext: EsContext, + listener: ActionListener[ActionResponse]): Unit = { + continue(esContext.task, esContext.action, esContext.actionRequest, listener) + } + + def continue(task: EsTask, + action: Action, + request: ActionRequest, + listener: ActionListener[ActionResponse]): Unit = { + chain.proceed(task, action.value, request, listener) + } + } + + sealed trait Error + object Error { + case object ImpersonatorsEngineNotConfigured extends Error + } +} + +final case class RequestSeemsToBeInvalid[T: ClassTag](message: String, cause: Throwable = null) + extends IllegalStateException(s"Request '${implicitly[ClassTag[T]].runtimeClass.getSimpleName}' cannot be handled; [msg: $message]", cause) \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/CurrentUserMetadataRequestHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/CurrentUserMetadataRequestHandler.scala new file mode 100644 index 0000000000..fca78db777 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/CurrentUserMetadataRequestHandler.scala @@ -0,0 +1,100 @@ +/* + * 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 + +import cats.data.NonEmptySet +import cats.implicits._ +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.common.io.stream.StreamOutput +import org.elasticsearch.xcontent.{ToXContent, ToXContentObject, XContentBuilder} +import tech.beshu.ror.accesscontrol.AccessControl.{ForbiddenCause, UserMetadataRequestResult} +import tech.beshu.ror.accesscontrol.blocks.BlockContext.CurrentUserMetadataRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.{MetadataValue, UserMetadata} +import tech.beshu.ror.accesscontrol.domain.CorrelationId +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.boot.ReadonlyRest.Engine +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.EsRequest +import tech.beshu.ror.es.handler.response.ForbiddenResponse.createRorNotEnabledResponse +import tech.beshu.ror.es.handler.response.ForbiddenResponse +import tech.beshu.ror.es.handler.response.ForbiddenResponse.Cause.fromMismatchedCause +import tech.beshu.ror.utils.LoggerOps._ + +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +class CurrentUserMetadataRequestHandler(engine: Engine, + esContext: EsContext) + extends Logging { + + def handle(request: RequestContext.Aux[CurrentUserMetadataRequestBlockContext] with EsRequest[CurrentUserMetadataRequestBlockContext]): Task[Unit] = { + engine.core.accessControl + .handleMetadataRequest(request) + .map { r => commitResult(r.result, request) } + } + + private def commitResult(result: UserMetadataRequestResult, + request: RequestContext): Unit = { + Try { + result match { + case UserMetadataRequestResult.Allow(userMetadata, _) => + onAllow(request, userMetadata) + case UserMetadataRequestResult.Forbidden(causes) => + onForbidden(causes) + case UserMetadataRequestResult.PassedThrough => + onPassThrough() + } + } match { + case Success(_) => + case Failure(ex) => + logger.errorEx(s"[${request.id.show}] ACL committing result failure", ex) + esContext.listener.onFailure(ex.asInstanceOf[Exception]) + } + } + + private def onAllow(requestContext: RequestContext, userMetadata: UserMetadata): Unit = { + esContext.listener.onResponse(new RRMetadataResponse(userMetadata, requestContext.correlationId)) + } + + private def onForbidden(causes: NonEmptySet[ForbiddenCause]): Unit = { + esContext.listener.onFailure(ForbiddenResponse.create( + causes = causes.toList.map(fromMismatchedCause), + aclStaticContext = engine.core.accessControl.staticContext + )) + } + + private def onPassThrough(): Unit = { + logger.warn(s"[${esContext.requestContextId}] Cannot handle the ${esContext.channel.request().path()} request because ReadonlyREST plugin was disabled in settings") + esContext.listener.onFailure(createRorNotEnabledResponse()) + } +} + +private class RRMetadataResponse(userMetadata: UserMetadata, + correlationId: CorrelationId) + extends ActionResponse with ToXContentObject { + + override def toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder = { + val sourceMap: Map[String, _] = + MetadataValue.read(userMetadata, correlationId).view.mapValues(MetadataValue.toAny).toMap + builder.map(sourceMap.asJava) + builder + } + + override def writeTo(out: StreamOutput): Unit = () +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala new file mode 100644 index 0000000000..bf6fbe9c43 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/RegularRequestHandler.scala @@ -0,0 +1,264 @@ +/* + * 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 + +import cats.data.NonEmptyList +import cats.implicits._ +import monix.eval.Task +import monix.execution.Scheduler +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.{ActionListener, ActionResponse} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.RegularRequestResult +import tech.beshu.ror.accesscontrol.blocks.BlockContext._ +import tech.beshu.ror.accesscontrol.blocks.BlockContextUpdater._ +import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater, FilteredResponseFields, ResponseTransformation} +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.boot.ReadonlyRest.Engine +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.{CustomResponse, UpdateResponse} +import tech.beshu.ror.es.handler.request.context.{EsRequest, ModificationResult} +import tech.beshu.ror.es.handler.response.ForbiddenResponse +import tech.beshu.ror.es.handler.response.ForbiddenResponse.Cause.fromMismatchedCause +import tech.beshu.ror.es.handler.response.ForbiddenResponse.{ForbiddenBlockMatch, OperationNotAllowed} +import tech.beshu.ror.es.utils.ThreadContextOps._ +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged +import tech.beshu.ror.utils.LoggerOps._ +import tech.beshu.ror.utils.ScalaOps._ + +import java.time.{Duration, Instant} +import scala.util.{Failure, Success, Try} + +class RegularRequestHandler(engine: Engine, + esContext: EsContext, + threadPool: ThreadPool) + (implicit scheduler: Scheduler) + extends Logging { + + def handle[B <: BlockContext : BlockContextUpdater](request: RequestContext.Aux[B] with EsRequest[B]): Task[Unit] = { + engine.core.accessControl + .handleRegularRequest(request) + .map { r => + threadPool.getThreadContext.stashAndMergeResponseHeaders(esContext).bracket { _ => + commitResult(r.result, request) + } + } + } + + private def commitResult[B <: BlockContext : BlockContextUpdater](result: RegularRequestResult[B], + request: EsRequest[B] with RequestContext.Aux[B]): Unit = { + Try { + result match { + case allow: RegularRequestResult.Allow[B] => + onAllow(request, allow.blockContext) + case RegularRequestResult.ForbiddenBy(_, _) => + onForbidden(NonEmptyList.one(ForbiddenBlockMatch)) + case RegularRequestResult.ForbiddenByMismatched(causes) => + onForbidden(causes.toNonEmptyList.map(fromMismatchedCause)) + case RegularRequestResult.IndexNotFound() => + onIndexNotFound(request) + case RegularRequestResult.AliasNotFound() => + onAliasNotFound(request) + case RegularRequestResult.TemplateNotFound() => + onTemplateNotFound(request) + case RegularRequestResult.Failed(ex) => + esContext.listener.onFailure(ex.asInstanceOf[Exception]) + case RegularRequestResult.PassedThrough() => + proceed(esContext.listener) + } + } match { + case Success(_) => + case Failure(ex) => + logger.errorEx(s"[${request.id.show}] ACL committing result failure", ex) + esContext.listener.onFailure(ex.asInstanceOf[Exception]) + } + } + + private def onAllow[B <: BlockContext](request: EsRequest[B] with RequestContext.Aux[B], + blockContext: B): Unit = { + configureResponseTransformations(blockContext.responseTransformations) + request.modifyUsing(blockContext) match { + case ModificationResult.Modified => + proceed() + case ModificationResult.ShouldBeInterrupted => + onForbidden(NonEmptyList.one(OperationNotAllowed)) + case ModificationResult.CannotModify => + logger.error(s"[${request.id.show}] Cannot modify incoming request. Passing it could lead to a security leak. Report this issue as fast as you can.") + onForbidden(NonEmptyList.one(OperationNotAllowed)) + case CustomResponse(response) => + respond(response) + case UpdateResponse(updateFunc) => + proceed(new UpdateResponseListener(updateFunc)) + } + } + + private def onForbidden(causes: NonEmptyList[ForbiddenResponse.Cause]): Unit = { + esContext.listener.onFailure(ForbiddenResponse.create(causes.toList, engine.core.accessControl.staticContext)) + } + + private def onIndexNotFound[B <: BlockContext : BlockContextUpdater](request: EsRequest[B] with RequestContext.Aux[B]): Unit = { + BlockContextUpdater[B] match { + case GeneralIndexRequestBlockContextUpdater => + handleIndexNotFoundForGeneralIndexRequest(request.asInstanceOf[EsRequest[GeneralIndexRequestBlockContext] with RequestContext.Aux[GeneralIndexRequestBlockContext]]) + case FilterableRequestBlockContextUpdater => + handleIndexNotFoundForSearchRequest(request.asInstanceOf[EsRequest[FilterableRequestBlockContext] with RequestContext.Aux[FilterableRequestBlockContext]]) + case FilterableMultiRequestBlockContextUpdater => + handleIndexNotFoundForMultiSearchRequest(request.asInstanceOf[EsRequest[FilterableMultiRequestBlockContext] with RequestContext.Aux[FilterableMultiRequestBlockContext]]) + case AliasRequestBlockContextUpdater => + handleIndexNotFoundForAliasRequest(request.asInstanceOf[EsRequest[AliasRequestBlockContext] with RequestContext.Aux[AliasRequestBlockContext]]) + case CurrentUserMetadataRequestBlockContextUpdater | + GeneralNonIndexRequestBlockContextUpdater | + RepositoryRequestBlockContextUpdater | + SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | + TemplateRequestBlockContextUpdater | + MultiIndexRequestBlockContextUpdater | + RorApiRequestBlockContextUpdater => + onForbidden(NonEmptyList.one(OperationNotAllowed)) + } + } + + private def onAliasNotFound[B <: BlockContext : BlockContextUpdater](request: EsRequest[B] with RequestContext.Aux[B]): Unit = { + BlockContextUpdater[B] match { + case AliasRequestBlockContextUpdater => + handleAliasNotFoundForAliasRequest(request.asInstanceOf[EsRequest[AliasRequestBlockContext] with RequestContext.Aux[AliasRequestBlockContext]]) + case FilterableMultiRequestBlockContextUpdater | + FilterableRequestBlockContextUpdater | + GeneralIndexRequestBlockContextUpdater | + CurrentUserMetadataRequestBlockContextUpdater | + GeneralNonIndexRequestBlockContextUpdater | + RepositoryRequestBlockContextUpdater | + SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | + TemplateRequestBlockContextUpdater | + MultiIndexRequestBlockContextUpdater | + RorApiRequestBlockContextUpdater => + onForbidden(NonEmptyList.one(OperationNotAllowed)) + } + } + + private def onTemplateNotFound[B <: BlockContext : BlockContextUpdater](request: EsRequest[B] with RequestContext.Aux[B]): Unit = { + BlockContextUpdater[B] match { + case TemplateRequestBlockContextUpdater => + handleTemplateNotFoundForTemplateRequest(request.asInstanceOf[EsRequest[TemplateRequestBlockContext] with RequestContext.Aux[TemplateRequestBlockContext]]) + case FilterableMultiRequestBlockContextUpdater | + FilterableRequestBlockContextUpdater | + GeneralIndexRequestBlockContextUpdater | + CurrentUserMetadataRequestBlockContextUpdater | + GeneralNonIndexRequestBlockContextUpdater | + RepositoryRequestBlockContextUpdater | + SnapshotRequestBlockContextUpdater | + DataStreamRequestBlockContextUpdater | + AliasRequestBlockContextUpdater | + MultiIndexRequestBlockContextUpdater | + RorApiRequestBlockContextUpdater => + onForbidden(NonEmptyList.one(OperationNotAllowed)) + } + } + + private def handleIndexNotFoundForGeneralIndexRequest(request: EsRequest[GeneralIndexRequestBlockContext] with RequestContext.Aux[GeneralIndexRequestBlockContext]): Unit = { + val modificationResult = request.modifyWhenIndexNotFound + handleModificationResult(modificationResult) + } + + private def handleIndexNotFoundForSearchRequest(request: EsRequest[FilterableRequestBlockContext] with RequestContext.Aux[FilterableRequestBlockContext]): Unit = { + val modificationResult = request.modifyWhenIndexNotFound + handleModificationResult(modificationResult) + } + + private def handleIndexNotFoundForMultiSearchRequest(request: EsRequest[FilterableMultiRequestBlockContext] with RequestContext.Aux[FilterableMultiRequestBlockContext]): Unit = { + val modificationResult = request.modifyWhenIndexNotFound + handleModificationResult(modificationResult) + } + + private def handleIndexNotFoundForAliasRequest(request: EsRequest[AliasRequestBlockContext] with RequestContext.Aux[AliasRequestBlockContext]): Unit = { + val modificationResult = request.modifyWhenIndexNotFound + handleModificationResult(modificationResult) + } + + private def handleAliasNotFoundForAliasRequest(request: EsRequest[AliasRequestBlockContext] with RequestContext.Aux[AliasRequestBlockContext]): Unit = { + val modificationResult = request.modifyWhenAliasNotFound + handleModificationResult(modificationResult) + } + + private def handleTemplateNotFoundForTemplateRequest(request: EsRequest[TemplateRequestBlockContext] with RequestContext.Aux[TemplateRequestBlockContext]): Unit = { + val modificationResult = request.modifyWhenTemplateNotFound + handleModificationResult(modificationResult) + } + + private def handleModificationResult(modificationResult: ModificationResult): Unit = { + modificationResult match { + case ModificationResult.Modified => + proceed() + case ModificationResult.CannotModify => + onForbidden(NonEmptyList.one(OperationNotAllowed)) + case ModificationResult.ShouldBeInterrupted => + onForbidden(NonEmptyList.one(OperationNotAllowed)) + case CustomResponse(response) => + respond(response) + case UpdateResponse(updateFunc) => + proceed(new UpdateResponseListener(updateFunc)) + } + } + + private def configureResponseTransformations(responseTransformations: List[ResponseTransformation]): Unit = { + responseTransformations.foreach { + case FilteredResponseFields(responseFieldsRestrictions) => + esContext.channel.setResponseFieldRestrictions(responseFieldsRestrictions) + } + } + + private def proceed(listener: ActionListener[ActionResponse] = esContext.listener): Unit = { + logRequestProcessingTime() + addProperHeader() + esContext.chain.continue(esContext, listener) + } + + private def addProperHeader(): Unit = { + if(esContext.action.isFieldCapsAction || esContext.action.isRollupAction || esContext.action.isGetSettingsAction) + threadPool.getThreadContext.addSystemAuthenticationHeader(esContext.nodeName) + else if (esContext.action.isXpackSecurityAction) + threadPool.getThreadContext.addRorUserAuthenticationHeader(esContext.nodeName) + else + threadPool.getThreadContext.addXpackSecurityAuthenticationHeader(esContext.nodeName) + } + + private def respond(response: ActionResponse): Unit = { + logRequestProcessingTime() + esContext.listener.onResponse(response) + } + + private def logRequestProcessingTime(): Unit = { + logger.debug(s"[${esContext.requestContextId}] Request processing time: ${Duration.between(esContext.timestamp, Instant.now()).toMillis}ms") + } + + private class UpdateResponseListener(update: ActionResponse => Task[ActionResponse]) extends ActionListener[ActionResponse] { + override def onResponse(response: ActionResponse): Unit = doPrivileged { + val stashedContext = threadPool.getThreadContext.stashAndMergeResponseHeaders(esContext) + update(response) runAsync { + case Right(updatedResponse) => + stashedContext.restore() + esContext.listener.onResponse(updatedResponse) + case Left(ex) => + stashedContext.close() + onFailure(new Exception(ex)) + } + } + + override def onFailure(e: Exception): Unit = esContext.listener.onFailure(e) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/RorNotAvailableRequestHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/RorNotAvailableRequestHandler.scala new file mode 100644 index 0000000000..a8727bb24a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/RorNotAvailableRequestHandler.scala @@ -0,0 +1,53 @@ +/* + * 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 + +import tech.beshu.ror.configuration.RorBootConfiguration +import tech.beshu.ror.configuration.RorBootConfiguration.{RorFailedToStartResponse, RorNotStartedResponse} +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.response.{ForbiddenResponse, ServiceNotAvailableResponse} + +final class RorNotAvailableRequestHandler(config: RorBootConfiguration) { + + def handleRorNotReadyYet(esContext: EsContext): Unit = { + val response = prepareNotReadyYetResponse() + esContext.listener.onFailure(response) + } + + def handleRorFailedToStart(esContext: EsContext): Unit = { + val response = prepareFailedToStartResponse() + esContext.listener.onFailure(response) + } + + private def prepareNotReadyYetResponse() = { + config.rorNotStartedResponse.httpCode match { + case RorNotStartedResponse.HttpCode.`403` => + ForbiddenResponse.createRorNotReadyYetResponse() + case RorNotStartedResponse.HttpCode.`503` => + ServiceNotAvailableResponse.createRorNotReadyYetResponse() + } + } + + private def prepareFailedToStartResponse() = { + config.rorFailedToStartResponse.httpCode match { + case RorFailedToStartResponse.HttpCode.`403` => + ForbiddenResponse.createRorStartingFailureResponse() + case RorFailedToStartResponse.HttpCode.`503` => + ServiceNotAvailableResponse.createRorStartingFailureResponse() + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/RestRequestOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/RestRequestOps.scala new file mode 100644 index 0000000000..bff5ab3ea6 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/RestRequestOps.scala @@ -0,0 +1,35 @@ +/* + * 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.rest.RestRequest +import tech.beshu.ror.accesscontrol.domain.Header + +import scala.jdk.CollectionConverters._ + +object RestRequestOps { + implicit class HeadersOps(val request: RestRequest) extends AnyVal { + def allHeaders(): Set[Header] = Header.fromRawHeaders( + request + .getHeaders.asScala + .view + .mapValues(_.asScala.toList) + .toMap + ) + } +} + diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/SearchRequestOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/SearchRequestOps.scala new file mode 100644 index 0000000000..2b5c7eaf89 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/SearchRequestOps.scala @@ -0,0 +1,213 @@ +/* + * 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 cats.data.NonEmptyList +import cats.implicits._ +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.search.SearchRequest +import org.elasticsearch.index.query.{AbstractQueryBuilder, QueryBuilder, QueryBuilders} +import org.elasticsearch.search.aggregations.AggregatorFactories +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder +import org.elasticsearch.search.builder.SearchSourceBuilder +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage.{CannotExtractFields, NotUsingFields, UsedField, UsingFields} +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.Strategy.{BasedOnBlockContextOnly, FlsAtLuceneLevelApproach} +import tech.beshu.ror.accesscontrol.domain.{FieldLevelSecurity, Filter} +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.es.handler.request.queries.QueryFieldsUsage.instances._ +import tech.beshu.ror.es.handler.request.queries.QueryFieldsUsage.{Ops => QueryFieldsUsageOps} +import tech.beshu.ror.es.handler.request.queries.QueryWithModifiableFields.instances._ +import tech.beshu.ror.es.handler.request.queries.QueryWithModifiableFields.{Ops => QueryWithModifiableFieldsOps} +import tech.beshu.ror.es.handler.response.FLSContextHeaderHandler + +import java.util.UUID +import scala.jdk.CollectionConverters._ + +object SearchRequestOps extends Logging { + + implicit class QueryBuilderOps(val builder: Option[QueryBuilder]) extends AnyVal { + + def wrapQueryBuilder(filter: Option[Filter]) + (implicit requestId: RequestContext.Id): Option[QueryBuilder] = { + filter match { + case Some(definedFilter) => + val filterQuery = QueryBuilders.wrapperQuery(definedFilter.value.value) + val modifiedQuery: AbstractQueryBuilder[_] = provideNewQueryWithAppliedFilter(builder, filterQuery) + Some(modifiedQuery) + case None => + logger.debug(s"[${requestId.show}] No filter applied to query.") + builder + } + } + + private def provideNewQueryWithAppliedFilter(queryBuilder: Option[QueryBuilder], + filterQuery: QueryBuilder) = { + queryBuilder match { + case Some(requestedQuery) => + QueryBuilders.boolQuery() + .must(requestedQuery) + .filter(filterQuery) + case None => + QueryBuilders.constantScoreQuery(filterQuery) + } + } + } + + implicit class FilterOps(val request: SearchRequest) extends AnyVal { + + def applyFilterToQuery(filter: Option[Filter]) + (implicit requestId: RequestContext.Id): SearchRequest = { + Option(request.source().query()) + .wrapQueryBuilder(filter) + .foreach { newQueryBuilder => + request.source().query(newQueryBuilder) + } + request + } + } + + implicit class FieldsOps(val request: SearchRequest) extends AnyVal { + + def applyFieldLevelSecurity(fieldLevelSecurity: Option[FieldLevelSecurity]) + (implicit threadPool: ThreadPool, + requestId: RequestContext.Id): SearchRequest = { + fieldLevelSecurity match { + case Some(definedFields) => + definedFields.strategy match { + case FlsAtLuceneLevelApproach => + FLSContextHeaderHandler.addContextHeader(threadPool, definedFields.restrictions, requestId) + disableCaching(requestId) + case BasedOnBlockContextOnly.NotAllowedFieldsUsed(notAllowedFields) => + modifyNotAllowedFieldsInRequest(notAllowedFields) + case BasedOnBlockContextOnly.EverythingAllowed => + request + } + case None => + request + } + } + + def checkFieldsUsage(): RequestFieldsUsage = { + checkFieldsUsageBaseOnScrollExistence() + .getOrElse(checkFieldsUsageBaseOnRequestSource()) + } + + private def checkFieldsUsageBaseOnScrollExistence() = { + // we have to use Lucene when scroll is used + Option(request.scroll()).map(_ => CannotExtractFields) + } + + private def checkFieldsUsageBaseOnRequestSource() = { + Option(request.source()) match { + case Some(source) if source.hasScriptFields => + CannotExtractFields + case Some(source) => + source.fieldsUsageInAggregations |+| source.fieldsUsageInQuery + case None => + NotUsingFields + } + } + + private def modifyNotAllowedFieldsInRequest(notAllowedFields: NonEmptyList[UsedField.SpecificField]) = { + Option(request.source()) match { + case None => + request + case Some(sourceBuilder) => + request.source( + sourceBuilder + .modifyNotAllowedFieldsInQuery(notAllowedFields) + .modifyNotAllowedFieldsInAggregations(notAllowedFields) + ) + } + } + + private def disableCaching(requestId: RequestContext.Id) = { + logger.debug(s"[${requestId.show}] ACL uses context header for fields rule, will disable request cache for SearchRequest") + request.requestCache(false) + } + } + + private implicit class SearchSourceBuilderOps(val builder: SearchSourceBuilder) extends AnyVal { + + def modifyNotAllowedFieldsInQuery(notAllowedFields: NonEmptyList[UsedField.SpecificField]): SearchSourceBuilder = { + Option(builder.query()) match { + case None => + builder + case Some(currentQuery) => + val newQuery = currentQuery.handleNotAllowedFields(notAllowedFields) + builder.query(newQuery) + } + } + + def modifyNotAllowedFieldsInAggregations(notAllowedFields: NonEmptyList[UsedField.SpecificField]): SearchSourceBuilder = { + def modifyBuilder(aggregatorFactoryBuilder: AggregatorFactories.Builder) = { + import org.joor.Reflect._ + on(builder).set("aggregations", aggregatorFactoryBuilder) + builder + } + + Option(builder.aggregations()) match { + case None => + builder + case Some(aggregations) => + val aggregatorFactoryBuilder = new AggregatorFactories.Builder() + aggregations + .getAggregatorFactories.asScala + .foreach { + case f: ValuesSourceAggregationBuilder[_] if notAllowedFields.find(s => s.value == f.field()).isDefined => + aggregatorFactoryBuilder.addAggregator(f.field(s"${f.field()}_${UUID.randomUUID().toString}")) + case f => + aggregatorFactoryBuilder.addAggregator(f) + } + modifyBuilder(aggregatorFactoryBuilder) + } + } + + def hasScriptFields: Boolean = Option(builder.scriptFields()).exists(_.size() > 0) + + def fieldsUsageInQuery: RequestFieldsUsage = { + Option(builder.query()) match { + case None => NotUsingFields + case Some(query) => query.fieldsUsage + } + } + + def fieldsUsageInAggregations: RequestFieldsUsage = { + Option(builder.aggregations()) match { + case None => + NotUsingFields + case Some(aggregations) => + NonEmptyList + .fromList { + aggregations + .getAggregatorFactories.asScala + .flatMap { + case builder: ValuesSourceAggregationBuilder[_] => Option(builder.field()) :: Nil + case _ => Nil + } + .flatten + .map(UsedField.apply) + .toList + } + .map(UsingFields.apply) + .getOrElse(NotUsingFields) + } + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala new file mode 100644 index 0000000000..6f86832118 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/BaseEsRequestContext.scala @@ -0,0 +1,151 @@ +/* + * 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 + +import com.softwaremill.sttp.Method +import eu.timepit.refined.auto._ +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.search.SearchRequest +import org.elasticsearch.action.{CompositeIndicesRequest, IndicesRequest} +import squants.information.{Bytes, Information} +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.domain.DataStreamName.FullLocalDataStreamWithAliases +import tech.beshu.ror.accesscontrol.domain._ +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.RestRequestOps._ +import tech.beshu.ror.utils.RCUtils + +import java.time.Instant + +abstract class BaseEsRequestContext[B <: BlockContext](esContext: EsContext, + clusterService: RorClusterService) + extends RequestContext with Logging { + + override type BLOCK_CONTEXT = B + + private val restRequest = esContext.channel.request() + + override val timestamp: Instant = Instant.now() + + override val taskId: Long = esContext.task.getId + + override lazy implicit val id: RequestContext.Id = RequestContext.Id(esContext.requestContextId) + + override lazy val action: Action = esContext.action + + override lazy val headers: Set[Header] = restRequest.allHeaders() + + override lazy val remoteAddress: Option[Address] = + Option(restRequest.getHttpChannel) + .flatMap(c => Option(c.getRemoteAddress)) + .flatMap(isa => Option(isa.getAddress)) + .flatMap(a => Option(a.getHostAddress)) + .map { remoteHost => if (RCUtils.isLocalHost(remoteHost)) RCUtils.LOCALHOST else remoteHost } + .flatMap(Address.from) + + override lazy val localAddress: Address = + Option(restRequest.getHttpChannel) + .flatMap(c => Option(c.getLocalAddress)) + .flatMap(isa => Option(isa.getAddress)) + .flatMap(a => Option(a.getHostAddress)) + .flatMap(Address.from) + .getOrElse(throw new IllegalArgumentException(s"Cannot create IP or hostname")) + + override lazy val method: Method = Method(restRequest.method().name()) + + override lazy val uriPath: UriPath = + UriPath + .from(restRequest.path()) + .getOrElse(UriPath("/")) + + override lazy val contentLength: Information = Bytes(Option(restRequest.content()).map(_.length()).getOrElse(0)) + + override lazy val `type`: Type = Type { + val requestClazz = esContext.actionRequest.getClass + val simpleName = requestClazz.getSimpleName + simpleName.toLowerCase match { + case "request" => requestClazz.getName.split("\\.").toList.reverse.headOption.getOrElse(simpleName) + case _ => simpleName + } + } + + override lazy val content: String = Option(restRequest.content()).map(_.utf8ToString()).getOrElse("") + + override lazy val indexAttributes: Set[IndexAttribute] = { + esContext.actionRequest match { + case req: IndicesRequest => indexAttributesFrom(req) + case _ => Set.empty + } + } + + override lazy val allIndicesAndAliases: Set[FullLocalIndexWithAliases] = { + clusterService.allIndicesAndAliases + } + + override lazy val allRemoteIndicesAndAliases: Task[Set[FullRemoteIndexWithAliases]] = + clusterService.allRemoteIndicesAndAliases.memoize + + override lazy val allDataStreamsAndAliases: Set[FullLocalDataStreamWithAliases] = { + clusterService.allDataStreamsAndAliases + } + + override lazy val allRemoteDataStreamsAndAliases: Task[Set[DataStreamName.FullRemoteDataStreamWithAliases]] = + clusterService.allRemoteDataStreamsAndAliases.memoize + + override lazy val allTemplates: Set[Template] = clusterService.allTemplates + + override lazy val allSnapshots: Map[RepositoryName.Full, Set[SnapshotName.Full]] = clusterService.allSnapshots + + override lazy val isCompositeRequest: Boolean = esContext.actionRequest.isInstanceOf[CompositeIndicesRequest] + + override lazy val isAllowedForDLS: Boolean = { + esContext.actionRequest match { + case _ if !isReadOnlyRequest => false + case sr: SearchRequest if sr.source() == null => true + case sr: SearchRequest if sr.source.profile || (sr.source.suggest != null && !sr.source.suggest.getSuggestions.isEmpty) => false + case _ => true + } + } + + protected def indexAttributesFrom(request: IndicesRequest): Set[IndexAttribute] = { + val wildcardOptions = request + .indicesOptions() + .wildcardOptions() + + Option.when(wildcardOptions.matchOpen())(IndexAttribute.Opened: IndexAttribute).toSet ++ + Option.when(wildcardOptions.matchClosed())(IndexAttribute.Closed: IndexAttribute).toSet + } + + protected def indicesOrWildcard(indices: Set[ClusterIndexName]): Set[ClusterIndexName] = { + if (indices.nonEmpty) indices else Set(ClusterIndexName.Local.wildcard) + } + + protected def repositoriesOrWildcard(repositories: Set[RepositoryName]): Set[RepositoryName] = { + if (repositories.nonEmpty) repositories else Set(RepositoryName.all) + } + + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/EsRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/EsRequest.scala new file mode 100644 index 0000000000..c318d5ee97 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/EsRequest.scala @@ -0,0 +1,75 @@ +/* + * 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 + +import cats.implicits._ +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext + +import scala.util.Try + +trait EsRequest[B <: BlockContext] extends Logging { + implicit def threadPool: ThreadPool + + final def modifyUsing(blockContext: B): ModificationResult = { + modifyCommonParts(blockContext) + Try(modifyRequest(blockContext)) + .fold( + ex => { + logger.error(s"[${blockContext.requestContext.id.show}] Cannot modify request with filtered data", ex) + ModificationResult.CannotModify + }, + identity + ) + } + + def modifyWhenIndexNotFound: ModificationResult = ModificationResult.CannotModify + + def modifyWhenAliasNotFound: ModificationResult = ModificationResult.CannotModify + + def modifyWhenTemplateNotFound: ModificationResult = ModificationResult.CannotModify + + protected def modifyRequest(blockContext: B): ModificationResult + + private def modifyCommonParts(blockContext: B): Unit = { + modifyResponseHeaders(blockContext) + } + + private def modifyResponseHeaders(blockContext: B): Unit = { + val threadContext = threadPool.getThreadContext + blockContext.responseHeaders.foreach(header => + threadContext.addResponseHeader(header.name.value.value, header.value.value)) + } +} + +sealed trait ModificationResult +object ModificationResult { + case object Modified extends ModificationResult + case object CannotModify extends ModificationResult + case object ShouldBeInterrupted extends ModificationResult + final case class CustomResponse(response: ActionResponse) extends ModificationResult + final case class UpdateResponse(update: ActionResponse => Task[ActionResponse]) extends ModificationResult + + object UpdateResponse { + def using(update: ActionResponse => ActionResponse): UpdateResponse = { + UpdateResponse(response => Task.now(update(response))) + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..9a91456223 --- /dev/null +++ b/es813x/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, _) => + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseFilterableEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseFilterableEsRequestContext.scala new file mode 100644 index 0000000000..fec7a02057 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseFilterableEsRequestContext.scala @@ -0,0 +1,94 @@ +/* + * 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.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.FilterableRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.{FieldLevelSecurity, Filter, ClusterIndexName} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +abstract class BaseFilterableEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[FilterableRequestBlockContext](esContext, clusterService) + with EsRequest[FilterableRequestBlockContext] { + + override val initialBlockContext: FilterableRequestBlockContext = FilterableRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + { + import tech.beshu.ror.accesscontrol.show.logs._ + val indices = indicesOrWildcard(indicesFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered indices: ${indices.map(_.show).mkString(",")}") + indices + }, + Set(ClusterIndexName.Local.wildcard), + None, + None, + requestFieldsUsage + ) + + override def modifyWhenIndexNotFound: ModificationResult = { + if (aclContext.doesRequirePassword) { + val nonExistentIndex = initialBlockContext.randomNonexistentIndex() + if (nonExistentIndex.hasWildcard) { + val nonExistingIndices = NonEmptyList + .fromList(initialBlockContext.nonExistingIndicesFromInitialIndices().toList) + .getOrElse(NonEmptyList.of(nonExistentIndex)) + update(actionRequest, nonExistingIndices, initialBlockContext.filter, initialBlockContext.fieldLevelSecurity) + Modified + } else { + ShouldBeInterrupted + } + } else { + update(actionRequest, NonEmptyList.of(initialBlockContext.randomNonexistentIndex()), initialBlockContext.filter, initialBlockContext.fieldLevelSecurity) + Modified + } + } + + override protected def modifyRequest(blockContext: FilterableRequestBlockContext): ModificationResult = { + NonEmptyList.fromList(blockContext.filteredIndices.toList) match { + case Some(indices) => + update(actionRequest, indices, blockContext.filter, blockContext.fieldLevelSecurity) + case None => + logger.warn(s"[${id.show}] empty list of indices produced, so we have to interrupt the request processing") + ShouldBeInterrupted + } + } + + protected def indicesFrom(request: R): Set[ClusterIndexName] + + protected def update(request: R, + indices: NonEmptyList[ClusterIndexName], + filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]): ModificationResult + + protected def requestFieldsUsage: RequestFieldsUsage +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseIndicesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseIndicesEsRequestContext.scala new file mode 100644 index 0000000000..6be5e780a9 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseIndicesEsRequestContext.scala @@ -0,0 +1,97 @@ +/* + * 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.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.GeneralIndexRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +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.ShouldBeInterrupted +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +abstract class BaseIndicesEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[GeneralIndexRequestBlockContext](esContext, clusterService) + with EsRequest[GeneralIndexRequestBlockContext] { + + override val initialBlockContext: GeneralIndexRequestBlockContext = GeneralIndexRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + { + import tech.beshu.ror.accesscontrol.show.logs._ + val indices = indicesOrWildcard(indicesFrom(actionRequest)) + logger.debug(s"[${id.show}] Discovered indices: ${indices.map(_.show).mkString(",")}") + indices + }, + Set(ClusterIndexName.Local.wildcard) + ) + + override def modifyWhenIndexNotFound: ModificationResult = { + if (aclContext.doesRequirePassword) { + val nonExistentIndex = initialBlockContext.randomNonexistentIndex() + if (nonExistentIndex.hasWildcard) { + val nonExistingIndices = NonEmptyList + .fromList(initialBlockContext.nonExistingIndicesFromInitialIndices().toList) + .getOrElse(NonEmptyList.of(nonExistentIndex)) + update(actionRequest, nonExistingIndices, nonExistingIndices) + } else { + ShouldBeInterrupted + } + } else { + val randomNonexistingIndex = initialBlockContext.randomNonexistentIndex() + update(actionRequest, NonEmptyList.of(randomNonexistingIndex), NonEmptyList.of(randomNonexistingIndex)) + } + } + + override protected def modifyRequest(blockContext: GeneralIndexRequestBlockContext): ModificationResult = { + (for { + filteredIndices <- toSortedNonEmptyList(blockContext.filteredIndices) + allAllowedIndices <- toSortedNonEmptyList(blockContext.allAllowedIndices) + } yield (filteredIndices, allAllowedIndices)) match { + case Some((filteredIndices, allAllowedIndices)) => + update(actionRequest, filteredIndices, allAllowedIndices) + case None => + logger.warn(s"[${id.show}] empty list of indices produced, so we have to interrupt the request processing") + ShouldBeInterrupted + } + } + + protected def indicesFrom(request: R): Set[ClusterIndexName] + + protected def update(request: R, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult + + private def toSortedNonEmptyList[A: Ordering](values: Iterable[A]) = { + NonEmptyList.fromList(values.toList.sorted) + } + + private implicit val indexNameOrdering: Ordering[ClusterIndexName] = + Ordering.by[ClusterIndexName, String](_.stringify)(implicitly[Ordering[String]].reverse) + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseRepositoriesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseRepositoriesEsRequestContext.scala new file mode 100644 index 0000000000..cb7c91c2cf --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseRepositoriesEsRequestContext.scala @@ -0,0 +1,60 @@ +/* + * 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 cats.data.NonEmptyList +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.RepositoryRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.RepositoryName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.ShouldBeInterrupted +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +abstract class BaseRepositoriesEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[RepositoryRequestBlockContext](esContext, clusterService) + with EsRequest[RepositoryRequestBlockContext] { + + override val initialBlockContext: RepositoryRequestBlockContext = RepositoryRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + repositoriesOrWildcard(repositoriesFrom(actionRequest)) + ) + + override protected def modifyRequest(blockContext: RepositoryRequestBlockContext): ModificationResult = { + NonEmptyList.fromList(blockContext.repositories.toList) match { + case Some(repositories) => + update(actionRequest, repositories) + case None => + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request, because of empty repositories list.") + ShouldBeInterrupted + } + } + + protected def repositoriesFrom(request: R): Set[RepositoryName] + + protected def update(request: R, repositories: NonEmptyList[RepositoryName]): ModificationResult +} + diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseSingleIndexEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseSingleIndexEsRequestContext.scala new file mode 100644 index 0000000000..d9bcb6bd9e --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseSingleIndexEsRequestContext.scala @@ -0,0 +1,50 @@ +/* + * 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.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 + +abstract class BaseSingleIndexEsRequestContext[R <: ActionRequest](actionRequest: R, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[R](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: R): Set[ClusterIndexName] = Set(indexFrom(request)) + + override protected def update(request: R, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + if (filteredIndices.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one index. First was taken. The whole set of indices [${filteredIndices.toList.mkString(",")}]") + } + update(request, filteredIndices.head) + } + + protected def indexFrom(request: R): ClusterIndexName + + protected def update(request: R, index: ClusterIndexName): ModificationResult +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseSnapshotEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseSnapshotEsRequestContext.scala new file mode 100644 index 0000000000..7ff57eb489 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseSnapshotEsRequestContext.scala @@ -0,0 +1,51 @@ +/* + * 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 org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.SnapshotRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +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 BaseSnapshotEsRequestContext[T <: ActionRequest](actionRequest: T, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[SnapshotRequestBlockContext](esContext, clusterService) + with EsRequest[SnapshotRequestBlockContext] { + + override val initialBlockContext: SnapshotRequestBlockContext = SnapshotRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + snapshotsOrWildcard(snapshotsFrom(actionRequest)), + repositoriesOrWildcard(repositoriesFrom(actionRequest)), + indicesFrom(actionRequest), + Set(ClusterIndexName.Local.wildcard) + ) + + protected def snapshotsFrom(request: T): Set[SnapshotName] + + protected def repositoriesFrom(request: T): Set[RepositoryName] + + protected def indicesFrom(request: T): Set[ClusterIndexName] +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseTemplatesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseTemplatesEsRequestContext.scala new file mode 100644 index 0000000000..89fb57410a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BaseTemplatesEsRequestContext.scala @@ -0,0 +1,46 @@ +/* + * 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 org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.TemplateOperation +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 BaseTemplatesEsRequestContext[R <: ActionRequest, T <: TemplateOperation](actionRequest: R, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[TemplateRequestBlockContext](esContext, clusterService) + with EsRequest[TemplateRequestBlockContext] { + + protected def templateOperationFrom(actionRequest: R): T + + override val initialBlockContext: TemplateRequestBlockContext = TemplateRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + templateOperationFrom(actionRequest), + identity, + Set.empty + ) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BulkEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BulkEsRequestContext.scala new file mode 100644 index 0000000000..7988b4a604 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BulkEsRequestContext.scala @@ -0,0 +1,104 @@ +/* + * 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.DocWriteRequest +import org.elasticsearch.action.bulk.BulkRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext.Indices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +import scala.jdk.CollectionConverters._ + +class BulkEsRequestContext(actionRequest: BulkRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[MultiIndexRequestBlockContext](esContext, clusterService) + with EsRequest[MultiIndexRequestBlockContext] { + + override lazy val initialBlockContext: MultiIndexRequestBlockContext = MultiIndexRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + indexPacksFrom(actionRequest) + ) + + override protected def modifyRequest(blockContext: MultiIndexRequestBlockContext): ModificationResult = { + val modifiedPacksOfIndices = blockContext.indexPacks + val requests = actionRequest.requests().asScala.toList + if (requests.size == modifiedPacksOfIndices.size) { + requests + .zip(modifiedPacksOfIndices) + .foldLeft(Modified: ModificationResult) { + case (Modified, (request, pack)) => updateRequest(request, pack) + case (_, _) => ShouldBeInterrupted + } + } else { + logger.error(s"[${id.show}] Cannot alter MultiGetRequest request, because origin request contained different " + + s"number of requests, than altered one. This can be security issue. So, it's better for forbid the request") + ShouldBeInterrupted + } + } + + private def indexPacksFrom(request: BulkRequest): List[Indices] = { + request + .requests().asScala + .map { r => Indices.Found(indicesFrom(r)) } + .toList + } + + private def indicesFrom(request: DocWriteRequest[_]): Set[domain.ClusterIndexName] = { + val requestIndices = request.indices.flatMap(ClusterIndexName.fromString).toSet + indicesOrWildcard(requestIndices) + } + + private def updateRequest(request: DocWriteRequest[_], indexPack: Indices): ModificationResult = { + indexPack match { + case Indices.Found(indices) => + NonEmptyList.fromList(indices.toList) match { + case Some(nel) => + updateRequestWithIndices(request, nel) + Modified + case None => + logger.error(s"[${id.show}] Cannot alter MultiGetRequest request, because empty list of indices was found") + ShouldBeInterrupted + } + case Indices.NotFound => + logger.error(s"[${id.show}] Cannot alter MultiGetRequest request, because no allowed indices were found") + ShouldBeInterrupted + } + } + + private def updateRequestWithIndices(request: DocWriteRequest[_], indices: NonEmptyList[ClusterIndexName]) = { + if (indices.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one index. First was taken. The whole set of indices [${indices.toList.mkString(",")}]") + } + request.index(indices.head.stringify) + } + +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BulkShardEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BulkShardEsRequestContext.scala new file mode 100644 index 0000000000..861fcbc727 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/BulkShardEsRequestContext.scala @@ -0,0 +1,73 @@ +/* + * 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.bulk.BulkShardRequest +import org.elasticsearch.index.Index +import org.elasticsearch.threadpool.ThreadPool +import org.reflections.ReflectionUtils +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.{CannotModify, Modified} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +class BulkShardEsRequestContext(actionRequest: BulkShardRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[BulkShardRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: BulkShardRequest): Set[ClusterIndexName] = { + request.indices().asSafeSet.flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: BulkShardRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + tryUpdate(request, filteredIndices) match { + case Success(_) => + Modified + case Failure(ex) => + logger.error(s"[${id.show}] Cannot modify BulkShardRequest", ex) + CannotModify + } + } + + private def tryUpdate(request: BulkShardRequest, indices: NonEmptyList[ClusterIndexName]) = { + val singleIndex = indices.head + val uuid = clusterService.indexOrAliasUuids(singleIndex).toList.head + ReflectionUtils + .getAllFields(request.shardId().getClass, ReflectionUtils.withName("index")).asScala + .foldLeft(Try(())) { + case (Success(_), field) => + field.setAccessible(true) + Try(field.set(request.shardId(), new Index(singleIndex.stringify, uuid))) + case (left, _) => + left + } + } + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterAllocationExplainEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterAllocationExplainEsRequestContext.scala new file mode 100644 index 0000000000..b999f04ee0 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterAllocationExplainEsRequestContext.scala @@ -0,0 +1,65 @@ +/* + * 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 cats.data.NonEmptyList +import org.elasticsearch.action.admin.cluster.allocation.ClusterAllocationExplainRequest +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} + +class ClusterAllocationExplainEsRequestContext(actionRequest: ClusterAllocationExplainRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ClusterAllocationExplainRequest): Set[ClusterIndexName] = + getIndexFrom(request).toSet + + override protected def update(request: ClusterAllocationExplainRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + getIndexFrom(request) match { + case Some(_) => + if (filteredIndices.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one index. First was taken. The whole set of indices [${filteredIndices.toList.mkString(",")}]") + } + updateIndexIn(request, filteredIndices.head) + Modified + case None if filteredIndices.exists(_ === ClusterIndexName.Local.wildcard) => + Modified + case None => + logger.error(s"[${id.show}] Cluster allocation explain request without index name is unavailable when block contains `indices` rule") + ShouldBeInterrupted + } + } + + private def getIndexFrom(request: ClusterAllocationExplainRequest) = { + Option(request.getIndex).flatMap(ClusterIndexName.fromString) + } + + private def updateIndexIn(request: ClusterAllocationExplainRequest, indexName: ClusterIndexName) = { + request.setIndex(indexName.stringify) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterRerouteEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterRerouteEsRequestContext.scala new file mode 100644 index 0000000000..b22383b7c9 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterRerouteEsRequestContext.scala @@ -0,0 +1,94 @@ +/* + * 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.admin.cluster.reroute.ClusterRerouteRequest +import org.elasticsearch.cluster.routing.allocation.command._ +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 + +import scala.jdk.CollectionConverters._ + +class ClusterRerouteEsRequestContext(actionRequest: ClusterRerouteRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ClusterRerouteRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ClusterRerouteRequest): Set[ClusterIndexName] = { + request + .getCommands.commands().asScala + .flatMap(indexFrom) + .toSet + } + + override protected def update(request: ClusterRerouteRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + val modifiedCommands = request + .getCommands.commands().asScala.toSeq + .map(modifiedCommand(_, filteredIndices)) + request.commands(new AllocationCommands(modifiedCommands: _*)) + Modified + } + + private def modifiedCommand(command: AllocationCommand, allowedIndices: NonEmptyList[ClusterIndexName]) = { + val indexFromCommand = indexFrom(command) + indexFromCommand match { + case None => command + case Some(index) if allowedIndices.exists(_ === index) => command + case Some(_) => setNonExistentIndexFor(command) + } + } + + private def indexFrom(command: AllocationCommand) = { + val indexNameStr = command match { + case c: CancelAllocationCommand => c.index() + case c: MoveAllocationCommand => c.index() + case c: AllocateEmptyPrimaryAllocationCommand => c.index() + case c: AllocateReplicaAllocationCommand => c.index() + case c: AllocateStalePrimaryAllocationCommand => c.index() + } + ClusterIndexName.fromString(indexNameStr) + } + + private def setNonExistentIndexFor(command: AllocationCommand) = { + val randomIndex = indexFrom(command) + .map(_.randomNonexistentIndex()) + .getOrElse(ClusterIndexName.Local.randomNonexistentIndex()) + command match { + case c: CancelAllocationCommand => + new CancelAllocationCommand(randomIndex.stringify, c.shardId(), c.node(), c.allowPrimary()) + case c: MoveAllocationCommand => + new MoveAllocationCommand(randomIndex.stringify, c.shardId(), c.fromNode(), c.toNode) + case c: AllocateEmptyPrimaryAllocationCommand => + new AllocateEmptyPrimaryAllocationCommand(randomIndex.stringify, c.shardId(), c.node(), c.acceptDataLoss()) + case c: AllocateReplicaAllocationCommand => + new AllocateReplicaAllocationCommand(randomIndex.stringify, c.shardId(), c.node()) + case c: AllocateStalePrimaryAllocationCommand => + new AllocateStalePrimaryAllocationCommand(randomIndex.stringify, c.shardId(), c.node(), c.acceptDataLoss()) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterStateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterStateEsRequestContext.scala new file mode 100644 index 0000000000..ba3c40b77e --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ClusterStateEsRequestContext.scala @@ -0,0 +1,54 @@ +/* + * 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.admin.cluster.state.ClusterStateRequest +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 +import tech.beshu.ror.utils.ScalaOps._ + +class ClusterStateEsRequestContext(actionRequest: ClusterStateRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ClusterStateRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ClusterStateRequest): Set[ClusterIndexName] = { + request.indices.asSafeSet.flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: ClusterStateRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + indicesFrom(request).toList match { + case Nil if filteredIndices.exists(_ === ClusterIndexName.Local.wildcard) => + // hack: when empty indices list is replaced with wildcard index, returned result is wrong + Modified + case _ => + request.indices(filteredIndices.toList.map(_.stringify): _*) + Modified + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDocumentEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDocumentEsRequestContext.scala new file mode 100644 index 0000000000..f418d70119 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DeleteDocumentEsRequestContext.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.es.handler.request.context.types + +import org.elasticsearch.action.delete.DeleteRequest +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.RequestSeemsToBeInvalid +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 + +class DeleteDocumentEsRequestContext(actionRequest: DeleteRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSingleIndexEsRequestContext[DeleteRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indexFrom(request: DeleteRequest): ClusterIndexName = { + ClusterIndexName + .fromString(actionRequest.index()) + .getOrElse { + throw RequestSeemsToBeInvalid[DeleteRequest]("Invalid index name") + } + } + + override protected def update(actionRequest: DeleteRequest, index: ClusterIndexName): ModificationResult = { + actionRequest.index(index.stringify) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DummyCompositeIndicesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DummyCompositeIndicesEsRequestContext.scala new file mode 100644 index 0000000000..19ac71d4cf --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/DummyCompositeIndicesEsRequestContext.scala @@ -0,0 +1,41 @@ +/* + * 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 org.elasticsearch.action.{ActionRequest, CompositeIndicesRequest} +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 + +class DummyCompositeIndicesEsRequestContext(actionRequest: ActionRequest with CompositeIndicesRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ActionRequest with CompositeIndicesRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ActionRequest with CompositeIndicesRequest): Set[ClusterIndexName] = Set.empty + + override protected def update(request: ActionRequest with CompositeIndicesRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = Modified +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GeneralNonIndexEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GeneralNonIndexEsRequestContext.scala new file mode 100644 index 0000000000..f20b6c75b7 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GeneralNonIndexEsRequestContext.scala @@ -0,0 +1,45 @@ +/* + * 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 org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.GeneralNonIndexRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +import scala.annotation.nowarn + +class GeneralNonIndexEsRequestContext(@nowarn("cat=unused") actionRequest: ActionRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[GeneralNonIndexRequestBlockContext](esContext, clusterService) + with EsRequest[GeneralNonIndexRequestBlockContext] { + + override val initialBlockContext: GeneralNonIndexRequestBlockContext = GeneralNonIndexRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty + ) + + override protected def modifyRequest(blockContext: GeneralNonIndexRequestBlockContext): ModificationResult = Modified +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetAliasesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetAliasesEsRequestContext.scala new file mode 100644 index 0000000000..671eb64e5b --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetAliasesEsRequestContext.scala @@ -0,0 +1,163 @@ +/* + * 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 monix.eval.Task +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.action.admin.indices.alias.get.{GetAliasesRequest, GetAliasesResponse} +import org.elasticsearch.cluster.metadata.{AliasMetadata, DataStreamAlias} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.{AliasRequestBlockContext, RandomIndexBasedOnBlockContextIndices} +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.show.logs._ +import tech.beshu.ror.accesscontrol.utils.IndicesListOps._ +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted, UpdateResponse} +import tech.beshu.ror.es.handler.request.context.types.utils.FilterableAliasesMap._ +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} +import tech.beshu.ror.utils.ScalaOps._ + +import java.util.{List => JList} + +class GetAliasesEsRequestContext(actionRequest: GetAliasesRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[AliasRequestBlockContext](esContext, clusterService) + with EsRequest[AliasRequestBlockContext] { + + override val initialBlockContext: AliasRequestBlockContext = AliasRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + { + val indices = aliasesFrom(actionRequest) + logger.debug(s"[${id.show}] Discovered aliases: ${indices.map(_.show).mkString(",")}") + indices + }, + { + val indices = indicesFrom(actionRequest) + logger.debug(s"[${id.show}] Discovered indices: ${indices.map(_.show).mkString(",")}") + indices + }, + ) + + override protected def modifyRequest(blockContext: AliasRequestBlockContext): ModificationResult = { + val result = for { + indices <- NonEmptyList.fromList(blockContext.indices.toList) + aliases <- NonEmptyList.fromList(blockContext.aliases.toList) + } yield (indices, aliases) + result match { + case Some((indices, aliases)) => + updateIndices(actionRequest, indices) + updateAliases(actionRequest, aliases) + UpdateResponse(updateAliasesResponse(aliases, _)) + case None => + logger.error(s"[${id.show}] At least one alias and one index has to be allowed. " + + s"Found allowed indices: [${blockContext.indices.map(_.show).mkString(",")}]." + + s"Found allowed aliases: [${blockContext.aliases.map(_.show).mkString(",")}]") + ShouldBeInterrupted + } + } + + override def modifyWhenIndexNotFound: ModificationResult = { + if (aclContext.doesRequirePassword) { + val nonExistentIndex = initialBlockContext.randomNonexistentIndex() + if (nonExistentIndex.hasWildcard) { + val nonExistingIndices = NonEmptyList + .fromList(initialBlockContext.nonExistingIndicesFromInitialIndices().toList) + .getOrElse(NonEmptyList.of(nonExistentIndex)) + updateIndices(actionRequest, nonExistingIndices) + Modified + } else { + ShouldBeInterrupted + } + } else { + updateIndices(actionRequest, NonEmptyList.of(initialBlockContext.randomNonexistentIndex())) + Modified + } + } + + override def modifyWhenAliasNotFound: ModificationResult = { + if (aclContext.doesRequirePassword) { + val nonExistentAlias = initialBlockContext.aliases.toList.randomNonexistentIndex() + if (nonExistentAlias.hasWildcard) { + val nonExistingAliases = NonEmptyList + .fromList(initialBlockContext.aliases.map(_.randomNonexistentIndex()).toList) + .getOrElse(NonEmptyList.of(nonExistentAlias)) + updateAliases(actionRequest, nonExistingAliases) + Modified + } else { + ShouldBeInterrupted + } + } else { + updateAliases(actionRequest, NonEmptyList.of(initialBlockContext.aliases.toList.randomNonexistentIndex())) + Modified + } + } + + private def updateIndices(request: GetAliasesRequest, indices: NonEmptyList[ClusterIndexName]): Unit = { + request.indices(indices.map(_.stringify).toList: _*) + } + + private def updateAliases(request: GetAliasesRequest, aliases: NonEmptyList[ClusterIndexName]): Unit = { + if (isRequestedEmptyAliasesSet(request)) { + // we don't need to do anything + } else { + request.aliases(aliases.map(_.stringify).toList: _*) + } + } + + private def updateAliasesResponse(allowedAliases: NonEmptyList[ClusterIndexName], + response: ActionResponse): Task[ActionResponse] = { + val (aliases, streams) = response match { + case aliasesResponse: GetAliasesResponse => + ( + aliasesResponse.getAliases.filterOutNotAllowedAliases(allowedAliases), + aliasesResponse.getDataStreamAliases + ) + case other => + logger.error(s"${id.show} Unexpected response type - expected: [${classOf[GetAliasesResponse].getSimpleName}], was: [${other.getClass.getSimpleName}]") + ( + Map.asEmptyJavaMap[String, JList[AliasMetadata]], + Map.asEmptyJavaMap[String, JList[DataStreamAlias]] + ) + } + Task.now(new GetAliasesResponse(aliases, streams)) + } + + private def indicesFrom(request: GetAliasesRequest) = { + indicesOrWildcard(request.indices().asSafeSet.flatMap(ClusterIndexName.fromString)) + } + + private def aliasesFrom(request: GetAliasesRequest) = { + indicesOrWildcard(rawRequestAliasesSet(request).flatMap(ClusterIndexName.fromString)) + } + + private def isRequestedEmptyAliasesSet(request: GetAliasesRequest) = { + rawRequestAliasesSet(request).isEmpty + } + + private def rawRequestAliasesSet(request: GetAliasesRequest) = request.aliases().asSafeSet +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetEsRequestContext.scala new file mode 100644 index 0000000000..9c8313f5b9 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetEsRequestContext.scala @@ -0,0 +1,97 @@ +/* + * 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 monix.eval.Task +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.action.get.{GetRequest, GetResponse} +import org.elasticsearch.action.index.IndexRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain.DocumentAccessibility.{Accessible, Inaccessible} +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, FieldLevelSecurity, Filter} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.response.DocumentApiOps.GetApi +import tech.beshu.ror.es.handler.response.DocumentApiOps.GetApi._ +import tech.beshu.ror.es.handler.request.context.ModificationResult + +class GetEsRequestContext(actionRequest: GetRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseFilterableEsRequestContext[GetRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def requestFieldsUsage: RequestFieldsUsage = RequestFieldsUsage.NotUsingFields + + override protected def indicesFrom(request: GetRequest): Set[ClusterIndexName] = { + val indexName = ClusterIndexName + .fromString(request.index()) + .getOrElse { + throw RequestSeemsToBeInvalid[IndexRequest]("Index name is invalid") + } + Set(indexName) + } + + override protected def update(request: GetRequest, + indices: NonEmptyList[ClusterIndexName], + filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]): ModificationResult = { + val indexName = indices.head + request.index(indexName.stringify) + ModificationResult.UpdateResponse(updateFunction(filter, fieldLevelSecurity)) + } + + private def updateFunction(filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]) + (actionResponse: ActionResponse): Task[ActionResponse] = { + filterResponse(filter, actionResponse) + .map(response => filterFieldsFromResponse(fieldLevelSecurity, response)) + } + + private def filterResponse(filter: Option[Filter], + actionResponse: ActionResponse): Task[ActionResponse] = { + (actionResponse, filter) match { + case (response: GetResponse, Some(definedFilter)) if response.isExists => + handleExistingResponse(response, definedFilter) + case _ => Task.now(actionResponse) + } + } + + private def handleExistingResponse(response: GetResponse, + definedFilter: Filter) = { + clusterService.verifyDocumentAccessibility(response.asDocumentWithIndex, definedFilter, id) + .map { + case Inaccessible => GetApi.doesNotExistResponse(response) + case Accessible => response + } + } + + private def filterFieldsFromResponse(fieldLevelSecurity: Option[FieldLevelSecurity], + actionResponse: ActionResponse): ActionResponse = { + (actionResponse, fieldLevelSecurity) match { + case (response: GetResponse, Some(definedFieldLevelSecurity)) if response.isExists => + response.filterFieldsUsing(definedFieldLevelSecurity.restrictions) + case _ => + actionResponse + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetIndexEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetIndexEsRequestContext.scala new file mode 100644 index 0000000000..4ab0cc900d --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetIndexEsRequestContext.scala @@ -0,0 +1,78 @@ +/* + * 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 monix.eval.Task +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.action.admin.indices.get.{GetIndexRequest, GetIndexResponse} +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.utils.FilterableAliasesMap._ +import tech.beshu.ror.utils.ScalaOps._ + +class GetIndexEsRequestContext(actionRequest: GetIndexRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[GetIndexRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: GetIndexRequest): Set[ClusterIndexName] = { + request + .indices().asSafeList + .flatMap(ClusterIndexName.fromString) + .toSet + } + + override protected def update(request: GetIndexRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + request.indices(filteredIndices.map(_.stringify).toList: _*) + ModificationResult.UpdateResponse(filterAliases(_, allAllowedIndices)) + } + + private def filterAliases(response: ActionResponse, + allAllowedAliases: NonEmptyList[ClusterIndexName]): Task[ActionResponse] = { + response match { + case getIndexResponse: GetIndexResponse => + Task.now(new GetIndexResponse( + getIndexResponse.indices(), + getIndexResponse.mappings(), + getIndexResponse.aliases().filterOutNotAllowedAliases(allowedAliases = allAllowedAliases), + getIndexResponse.settings(), + getIndexResponse.defaultSettings(), + getIndexResponse.dataStreams() + )) + case other => + logger.error(s"${id.show} Unexpected response type - expected: [${classOf[GetIndexResponse].getSimpleName}], was: [${other.getClass.getSimpleName}]") + Task.now(new GetIndexResponse( + Array.empty, + Map.asEmptyJavaMap, + Map.asEmptyJavaMap, + Map.asEmptyJavaMap, + Map.asEmptyJavaMap, + Map.asEmptyJavaMap + )) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetRollupCapsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetRollupCapsEsRequestContext.scala new file mode 100644 index 0000000000..6ee375820e --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetRollupCapsEsRequestContext.scala @@ -0,0 +1,61 @@ +/* + * 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 org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect._ +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.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified + +class GetRollupCapsEsRequestContext private(actionRequest: ActionRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSingleIndexEsRequestContext[ActionRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indexFrom(request: ActionRequest): ClusterIndexName = { + val indexStr = on(request).call("getIndexPattern").get[String]() + ClusterIndexName.fromString(indexStr) match { + case Some(index) => index + case None => + throw new RequestSeemsToBeInvalid[ActionRequest]("Cannot get non-empty index pattern from GetRollupCapsAction$Request") + } + } + + override protected def update(request: ActionRequest, index: ClusterIndexName): ModificationResult = { + on(request).set("indexPattern", index.stringify) + Modified + } +} + +object GetRollupCapsEsRequestContext { + + def unapply(arg: ReflectionBasedActionRequest): Option[GetRollupCapsEsRequestContext] = { + if (arg.esContext.actionRequest.getClass.getName.endsWith("GetRollupCapsAction$Request")) { + Some(new GetRollupCapsEsRequestContext(arg.esContext.actionRequest, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + } else { + None + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetRollupIndexCapsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetRollupIndexCapsEsRequestContext.scala new file mode 100644 index 0000000000..0812f0b0b7 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetRollupIndexCapsEsRequestContext.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.data.NonEmptyList +import org.elasticsearch.action.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect._ +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 + +class GetRollupIndexCapsEsRequestContext private(actionRequest: ActionRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ActionRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ActionRequest): Set[ClusterIndexName] = { + val indicesName = on(request).call("indices").get[Array[String]]() + indicesName.flatMap(ClusterIndexName.fromString).toSet + } + + override protected def update(request: ActionRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + on(request).call("indices", filteredIndices.map(_.stringify).toList.toArray) + Modified + } +} + +object GetRollupIndexCapsEsRequestContext { + def from(actionRequest: ActionRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + threadPool: ThreadPool): Option[GetRollupIndexCapsEsRequestContext] = { + if (actionRequest.getClass.getName.endsWith("GetRollupIndexCapsAction$Request")) { + Some(new GetRollupIndexCapsEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool)) + } else { + None + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetSettingsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetSettingsEsRequestContext.scala new file mode 100644 index 0000000000..77458cc0c3 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/GetSettingsEsRequestContext.scala @@ -0,0 +1,47 @@ +/* + * 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 org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest +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 +import tech.beshu.ror.utils.ScalaOps._ + +class GetSettingsEsRequestContext(actionRequest: GetSettingsRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[GetSettingsRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: GetSettingsRequest): Set[ClusterIndexName] = { + request.indices.asSafeSet.flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: GetSettingsRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + request.indices(filteredIndices.toList.map(_.stringify): _*) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndexEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndexEsRequestContext.scala new file mode 100644 index 0000000000..2f1982a7ca --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndexEsRequestContext.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.es.handler.request.context.types + +import org.elasticsearch.action.index.IndexRequest +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.RequestSeemsToBeInvalid +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 + +class IndexEsRequestContext(actionRequest: IndexRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSingleIndexEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indexFrom(request: IndexRequest): ClusterIndexName = { + ClusterIndexName + .fromString(request.index()) + .getOrElse { + throw RequestSeemsToBeInvalid[IndexRequest]("Index name is invalid") + } + } + + override protected def update(request: IndexRequest, index: ClusterIndexName): ModificationResult = { + request.index(index.stringify) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesAliasesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesAliasesEsRequestContext.scala new file mode 100644 index 0000000000..5fe9df7feb --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesAliasesEsRequestContext.scala @@ -0,0 +1,60 @@ +/* + * 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 cats.data.NonEmptyList +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest +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.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ + +class IndicesAliasesEsRequestContext(actionRequest: IndicesAliasesRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[IndicesAliasesRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + private lazy val originIndices = actionRequest + .getAliasActions.asScala + .flatMap { r => + r.indices.asSafeSet.flatMap(ClusterIndexName.fromString) ++ + r.aliases.asSafeList.flatMap(ClusterIndexName.fromString) + } + .toSet + + override protected def indicesFrom(request: IndicesAliasesRequest): Set[ClusterIndexName] = originIndices + + override protected def update(request: IndicesAliasesRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + if (originIndices == filteredIndices.toList.toSet) { + 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.") + ShouldBeInterrupted + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesReplaceableEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesReplaceableEsRequestContext.scala new file mode 100644 index 0000000000..68eaf77c4c --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesReplaceableEsRequestContext.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.es.handler.request.context.types + +import cats.data.NonEmptyList +import org.elasticsearch.action.{ActionRequest, IndicesRequest} +import org.elasticsearch.action.IndicesRequest.Replaceable +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 +import tech.beshu.ror.utils.ScalaOps._ + +class IndicesReplaceableEsRequestContext(actionRequest: ActionRequest with Replaceable, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ActionRequest with Replaceable](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ActionRequest with Replaceable): Set[ClusterIndexName] = { + request.asInstanceOf[IndicesRequest].indices.asSafeSet.flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: ActionRequest with Replaceable, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + request.indices(filteredIndices.toList.map(_.stringify): _*) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesShardStoresEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesShardStoresEsRequestContext.scala new file mode 100644 index 0000000000..36973f9be7 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesShardStoresEsRequestContext.scala @@ -0,0 +1,47 @@ +/* + * 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 org.elasticsearch.action.admin.indices.shards.IndicesShardStoresRequest +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 +import tech.beshu.ror.utils.ScalaOps._ + +class IndicesShardStoresEsRequestContext(actionRequest: IndicesShardStoresRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[IndicesShardStoresRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: IndicesShardStoresRequest): Set[ClusterIndexName] = { + request.indices.asSafeSet.flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: IndicesShardStoresRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + actionRequest.indices(filteredIndices.toList.map(_.stringify): _*) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesStatsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesStatsEsRequestContext.scala new file mode 100644 index 0000000000..e2572a64de --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/IndicesStatsEsRequestContext.scala @@ -0,0 +1,47 @@ +/* + * 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 org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest +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 +import tech.beshu.ror.utils.ScalaOps._ + +class IndicesStatsEsRequestContext(actionRequest: IndicesStatsRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[IndicesStatsRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: IndicesStatsRequest): Set[ClusterIndexName] = { + request.indices.asSafeSet.flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: IndicesStatsRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + request.indices(filteredIndices.map(_.stringify).toList: _*) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiGetEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiGetEsRequestContext.scala new file mode 100644 index 0000000000..5ecce7ae5c --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiGetEsRequestContext.scala @@ -0,0 +1,205 @@ +/* + * 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 monix.eval.Task +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.action.get.{MultiGetItemResponse, MultiGetRequest, MultiGetResponse} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.FilterableMultiRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext.Indices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.DocumentAccessibility.{Accessible, Inaccessible} +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain._ +import tech.beshu.ror.accesscontrol.utils.IndicesListOps._ +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.response.DocumentApiOps.GetApi +import tech.beshu.ror.es.handler.response.DocumentApiOps.GetApi._ +import tech.beshu.ror.es.handler.response.DocumentApiOps.MultiGetApi._ +import tech.beshu.ror.es.handler.request.context.ModificationResult.ShouldBeInterrupted +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +import scala.jdk.CollectionConverters._ + +class MultiGetEsRequestContext(actionRequest: MultiGetRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[FilterableMultiRequestBlockContext](esContext, clusterService) + with EsRequest[FilterableMultiRequestBlockContext] { + + private val requestFieldsUsage: RequestFieldsUsage = RequestFieldsUsage.NotUsingFields + + override lazy val initialBlockContext: FilterableMultiRequestBlockContext = FilterableMultiRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + indexPacks = indexPacksFrom(actionRequest), + filter = None, + fieldLevelSecurity = None, + requestFieldsUsage = requestFieldsUsage + ) + + override lazy val indexAttributes: Set[IndexAttribute] = { + // It may be a problem in some cases. We get all possible index attributes and we put them to one bag. + actionRequest + .getItems.asScala + .flatMap(indexAttributesFrom) + .toSet + } + + override protected def modifyRequest(blockContext: FilterableMultiRequestBlockContext): ModificationResult = { + val modifiedPacksOfIndices = blockContext.indexPacks + val items = actionRequest.getItems.asScala.toList + if (items.size == modifiedPacksOfIndices.size) { + items + .zip(modifiedPacksOfIndices) + .foreach { case (item, pack) => + updateItem(item, pack) + } + ModificationResult.UpdateResponse(updateFunction(blockContext.filter, blockContext.fieldLevelSecurity)) + } else { + logger.error( + s"""[${id.show}] Cannot alter MultiGetRequest request, because origin request contained different + |number of items, than altered one. This can be security issue. So, it's better for forbid the request""".stripMargin) + ShouldBeInterrupted + } + } + + private def indexPacksFrom(request: MultiGetRequest): List[Indices] = { + request + .getItems.asScala + .map { item => Indices.Found(indicesFrom(item)) } + .toList + } + + private def indicesFrom(item: MultiGetRequest.Item): Set[domain.ClusterIndexName] = { + val requestIndices = item.indices.flatMap(ClusterIndexName.fromString).toSet + indicesOrWildcard(requestIndices) + } + + private def updateItem(item: MultiGetRequest.Item, + indexPack: Indices): Unit = { + indexPack match { + case Indices.Found(indices) => + updateItemWithIndices(item, indices) + case Indices.NotFound => + updateItemWithNonExistingIndex(item) + } + } + + private def updateItemWithIndices(item: MultiGetRequest.Item, indices: Set[ClusterIndexName]) = { + indices.toList match { + case Nil => updateItemWithNonExistingIndex(item) + case index :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one index. First was taken. The whole set of indices [${indices.toList.mkString(",")}]") + } + item.index(index.stringify) + } + } + + private def updateItemWithNonExistingIndex(item: MultiGetRequest.Item): Unit = { + val originRequestIndices = indicesFrom(item).toList + val notExistingIndex = originRequestIndices.randomNonexistentIndex() + item.index(notExistingIndex.stringify) + } + + private def updateFunction(filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]) + (actionResponse: ActionResponse): Task[ActionResponse] = { + filterResponse(filter, actionResponse) + .map(response => filterFieldsFromResponse(fieldLevelSecurity, response)) + } + + private def filterResponse(filter: Option[Filter], + actionResponse: ActionResponse): Task[ActionResponse] = { + (actionResponse, filter) match { + case (response: MultiGetResponse, Some(definedFilter)) => + applyFilter(response, definedFilter) + case _ => + Task.now(actionResponse) + } + } + + private def applyFilter(response: MultiGetResponse, + definedFilter: Filter): Task[ActionResponse] = { + val originalResponses = response.getResponses.toList + + NonEmptyList.fromList(identifyDocumentsToVerifyUsing(originalResponses)) match { + case Some(existingDocumentsToVerify) => + clusterService.verifyDocumentsAccessibilities(existingDocumentsToVerify, definedFilter, id) + .map { results => + prepareNewResponse(originalResponses, results) + } + case None => + Task.now(response) + } + } + + private def identifyDocumentsToVerifyUsing(itemResponses: List[MultiGetItemResponse]) = { + itemResponses + .filter(requiresAdditionalVerification) + .map(_.asDocumentWithIndex) + .distinct + } + + private def requiresAdditionalVerification(item: MultiGetItemResponse) = { + !item.isFailed && item.getResponse.isExists + } + + private def prepareNewResponse(originalResponses: List[MultiGetItemResponse], + verificationResults: Map[DocumentWithIndex, DocumentAccessibility]) = { + val newResponses = originalResponses + .map(adjustResponseUsingResolvedAccessibility(verificationResults)) + .toArray + new MultiGetResponse(newResponses) + } + + private def adjustResponseUsingResolvedAccessibility(accessibilityPerDocument: Map[DocumentWithIndex, DocumentAccessibility]) + (item: MultiGetItemResponse) = { + accessibilityPerDocument.get(item.asDocumentWithIndex) match { + case None | Some(Accessible) => item + case Some(Inaccessible) => + val newResponse = GetApi.doesNotExistResponse(original = item.getResponse) + new MultiGetItemResponse(newResponse, null) + } + } + + private def filterFieldsFromResponse(fieldLevelSecurity: Option[FieldLevelSecurity], + actionResponse: ActionResponse): ActionResponse = { + (actionResponse, fieldLevelSecurity) match { + case (response: MultiGetResponse, Some(definedFieldLevelSecurity)) => + val newResponses = response.getResponses + .map { + case multiGetItem if !multiGetItem.isFailed => + val newGetResponse = multiGetItem.getResponse.filterFieldsUsing(definedFieldLevelSecurity.restrictions) + new MultiGetItemResponse(newGetResponse, null) + case other => other + } + new MultiGetResponse(newResponses) + case _ => + actionResponse + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiSearchEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiSearchEsRequestContext.scala new file mode 100644 index 0000000000..f06730888b --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiSearchEsRequestContext.scala @@ -0,0 +1,159 @@ +/* + * 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.ActionResponse +import org.elasticsearch.action.search.{MultiSearchRequest, MultiSearchResponse, SearchRequest} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.FilterableMultiRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext.Indices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage.NotUsingFields +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.Strategy.BasedOnBlockContextOnly +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, FieldLevelSecurity, Filter, IndexAttribute} +import tech.beshu.ror.accesscontrol.utils.IndicesListOps._ +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.response.SearchHitOps._ +import tech.beshu.ror.es.handler.request.SearchRequestOps._ +import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ + +class MultiSearchEsRequestContext(actionRequest: MultiSearchRequest, + esContext: EsContext, + clusterService: RorClusterService, + override implicit val threadPool: ThreadPool) + extends BaseEsRequestContext[FilterableMultiRequestBlockContext](esContext, clusterService) + with EsRequest[FilterableMultiRequestBlockContext] { + + override lazy val initialBlockContext: FilterableMultiRequestBlockContext = FilterableMultiRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty, + indexPacks = indexPacksFrom(actionRequest), + filter = None, + fieldLevelSecurity = None, + requestFieldsUsage = requestFieldsUsage + ) + + override lazy val indexAttributes: Set[IndexAttribute] = { + // It may be a problem in some cases. We get all possible index attributes and we put them to one bag. + actionRequest + .requests().asScala + .flatMap(indexAttributesFrom) + .toSet + } + + override protected def modifyRequest(blockContext: FilterableMultiRequestBlockContext): ModificationResult = { + val modifiedPacksOfIndices = blockContext.indexPacks + val requests = actionRequest.requests().asScala.toList + if (requests.size == modifiedPacksOfIndices.size) { + requests + .zip(modifiedPacksOfIndices) + .foreach { case (request, pack) => + updateRequest(request, pack, blockContext.filter, blockContext.fieldLevelSecurity) + } + ModificationResult.UpdateResponse.using(filterFieldsFromResponse(blockContext.fieldLevelSecurity)) + } else { + logger.error(s"[${id.show}] Cannot alter MultiSearchRequest request, because origin request contained different number of" + + s" inner requests, than altered one. This can be security issue. So, it's better for forbid the request") + ShouldBeInterrupted + } + } + + private def requestFieldsUsage: RequestFieldsUsage = { + NonEmptyList.fromList(actionRequest.requests().asScala.toList) match { + case Some(definedRequests) => + definedRequests + .map(_.checkFieldsUsage()) + .combineAll + case None => + NotUsingFields + } + } + + private def filterFieldsFromResponse(fieldLevelSecurity: Option[FieldLevelSecurity]) + (actionResponse: ActionResponse): ActionResponse = { + (actionResponse, fieldLevelSecurity) match { + case (response: MultiSearchResponse, Some(FieldLevelSecurity(restrictions, _: BasedOnBlockContextOnly))) => + response.getResponses + .filterNot(_.isFailure) + .flatMap(_.getResponse.getHits.getHits) + .foreach { hit => + hit + .filterSourceFieldsUsing(restrictions) + .filterDocumentFieldsUsing(restrictions) + } + response + case _ => + actionResponse + } + } + + override def modifyWhenIndexNotFound: ModificationResult = { + val requests = actionRequest.requests().asScala.toList + requests.foreach(updateRequestWithNonExistingIndex) + Modified + } + + private def indexPacksFrom(request: MultiSearchRequest): List[Indices] = { + request + .requests().asScala + .map { request => Indices.Found(indicesFrom(request)) } + .toList + } + + private def indicesFrom(request: SearchRequest) = { + val requestIndices = request.indices.asSafeSet.flatMap(ClusterIndexName.fromString) + indicesOrWildcard(requestIndices) + } + + private def updateRequest(request: SearchRequest, + indexPack: Indices, + filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]) = { + indexPack match { + case Indices.Found(indices) => + updateRequestWithIndices(request, indices) + case Indices.NotFound => + updateRequestWithNonExistingIndex(request) + } + request + .applyFilterToQuery(filter) + .applyFieldLevelSecurity(fieldLevelSecurity) + } + + private def updateRequestWithIndices(request: SearchRequest, indices: Set[ClusterIndexName]) = { + indices.toList match { + case Nil => updateRequestWithNonExistingIndex(request) + case nonEmptyIndicesList => request.indices(nonEmptyIndicesList.map(_.stringify): _*) + } + } + + private def updateRequestWithNonExistingIndex(request: SearchRequest): Unit = { + val originRequestIndices = indicesFrom(request).toList + val notExistingIndex = originRequestIndices.randomNonexistentIndex() + request.indices(notExistingIndex.stringify) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiTermVectorsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiTermVectorsEsRequestContext.scala new file mode 100644 index 0000000000..5c832a53ff --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/MultiTermVectorsEsRequestContext.scala @@ -0,0 +1,70 @@ +/* + * 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.termvectors.{MultiTermVectorsRequest, TermVectorsRequest} +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 scala.jdk.CollectionConverters._ + +class MultiTermVectorsEsRequestContext(actionRequest: MultiTermVectorsRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[MultiTermVectorsRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: MultiTermVectorsRequest): Set[ClusterIndexName] = { + request.getRequests.asScala.flatMap(r => ClusterIndexName.fromString(r.index())).toSet + } + + override protected def update(request: MultiTermVectorsRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + request.getRequests.removeIf { request => removeOrAlter(request, filteredIndices.toList.toSet) } + if (request.getRequests.asScala.isEmpty) { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request. All indices were filtered out.") + ShouldBeInterrupted + } else { + Modified + } + } + + private def removeOrAlter(request: TermVectorsRequest, + filteredIndices: Set[ClusterIndexName]): Boolean = { + val expandedIndicesOfRequest = clusterService.expandLocalIndices(ClusterIndexName.fromString(request.index()).toSet) + val remaining = expandedIndicesOfRequest.intersect(filteredIndices).toList + remaining match { + case Nil => + true + case index :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one index. First was taken. The whole set of indices [${remaining.mkString(",")}]") + } + request.index(index.stringify) + false + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PutRollupJobEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PutRollupJobEsRequestContext.scala new file mode 100644 index 0000000000..9e0edf2798 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/PutRollupJobEsRequestContext.scala @@ -0,0 +1,68 @@ +/* + * 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.ActionRequest +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect._ +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} + +class PutRollupJobEsRequestContext private(actionRequest: ActionRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ActionRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + private lazy val originIndices = { + val config = on(actionRequest).call("getConfig").get[AnyRef]() + val indexPattern = on(config).call("getIndexPattern").get[String]() + val rollupIndex = on(config).call("getRollupIndex").get[String]() + (ClusterIndexName.fromString(indexPattern) :: ClusterIndexName.fromString(rollupIndex) :: Nil).flatten.toSet + } + + override protected def indicesFrom(request: ActionRequest): Set[ClusterIndexName] = originIndices + + override protected def update(request: ActionRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + if(originIndices == filteredIndices.toList.toSet) { + 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.") + ShouldBeInterrupted + } + } +} + +object PutRollupJobEsRequestContext { + + def unapply(arg: ReflectionBasedActionRequest): Option[PutRollupJobEsRequestContext] = { + if (arg.esContext.actionRequest.getClass.getName.endsWith("PutRollupJobAction$Request")) { + Some(new PutRollupJobEsRequestContext(arg.esContext.actionRequest, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + } else { + None + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReflectionBasedActionRequest.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReflectionBasedActionRequest.scala new file mode 100644 index 0000000000..f239a22d14 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReflectionBasedActionRequest.scala @@ -0,0 +1,27 @@ +/* + * 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 org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext + +final case class ReflectionBasedActionRequest(esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + threadPool: ThreadPool) diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReflectionBasedIndicesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReflectionBasedIndicesEsRequestContext.scala new file mode 100644 index 0000000000..f8703933de --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReflectionBasedIndicesEsRequestContext.scala @@ -0,0 +1,123 @@ +/* + * 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 com.google.common.collect.Sets +import org.apache.logging.log4j.scala.Logging +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.ModificationResult.{Modified, ShouldBeInterrupted} +import tech.beshu.ror.utils.ReflecUtils + +import java.util.{List => JList} +import scala.jdk.CollectionConverters._ +import scala.util.Try + +class ReflectionBasedIndicesEsRequestContext private(actionRequest: ActionRequest, + indices: Set[ClusterIndexName], + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ActionRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ActionRequest): Set[ClusterIndexName] = indices + + override protected def update(request: ActionRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + if (tryUpdate(actionRequest, filteredIndices)) Modified + else { + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request. We're using reflection to modify the request indices and it fails. Please, report the issue.") + ShouldBeInterrupted + } + } + + private def tryUpdate(actionRequest: ActionRequest, indices: NonEmptyList[ClusterIndexName]) = { + // Optimistic reflection attempt + ReflecUtils.setIndices( + actionRequest, + Sets.newHashSet("index", "indices", "setIndex", "setIndices"), + indices.toList.map(_.stringify).toSet.asJava + ) + } +} + +object ReflectionBasedIndicesEsRequestContext extends Logging { + + def unapply(arg: ReflectionBasedActionRequest): Option[ReflectionBasedIndicesEsRequestContext] = { + indicesFrom(arg.esContext.actionRequest) + .map(new ReflectionBasedIndicesEsRequestContext(arg.esContext.actionRequest, _, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + } + + private def indicesFrom(request: ActionRequest) = { + getIndicesUsingMethod(request, methodName = "getIndices") + .orElse(getIndicesUsingField(request, fieldName = "indices")) + .orElse(getIndexUsingMethod(request, methodName = "getIndex")) + .orElse(getIndexUsingField(request, fieldName = "index")) + .map(indices => indices.toList.toSet.flatMap(ClusterIndexName.fromString)) + } + + private def getIndicesUsingMethod(request: ActionRequest, methodName: String) = { + callMethod[JList[String]](request, methodName) + .map { indices => + NonEmptyList.fromFoldable { + Option(indices).toList.flatMap(_.asScala) + } + } + .getOrElse(None) + } + + private def getIndicesUsingField(request: ActionRequest, fieldName: String) = { + getFieldValue[JList[String]](request, fieldName) + .map { indices => + NonEmptyList.fromFoldable { + Option(indices).toList.flatMap(_.asScala) + } + } + .getOrElse(None) + } + + private def getIndexUsingField(request: ActionRequest, fieldName: String) = { + getFieldValue[String](request, fieldName) + .map(index => NonEmptyList.fromFoldable(Option(index))) + .getOrElse(None) + } + + private def getIndexUsingMethod(request: ActionRequest, methodName: String) = { + callMethod[String](request, methodName) + .map(index => NonEmptyList.fromFoldable(Option(index))) + .getOrElse(None) + } + + private def callMethod[RESULT](obj: AnyRef, methodName: String) = { + import org.joor.Reflect._ + Try(on(obj).call(methodName).get[RESULT]) + } + + private def getFieldValue[RESULT](obj: AnyRef, fieldName: String) = { + import org.joor.Reflect._ + Try(on(obj).get[RESULT](fieldName)) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReindexEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReindexEsRequestContext.scala new file mode 100644 index 0000000000..f0ed21b823 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ReindexEsRequestContext.scala @@ -0,0 +1,67 @@ +/* + * 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.index.reindex.ReindexRequest +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.utils.ScalaOps._ + +class ReindexEsRequestContext(actionRequest: ReindexRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ReindexRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ReindexRequest): Set[ClusterIndexName] = { + val searchRequestIndices = request.getSearchRequest.indices.asSafeSet + val indexOfIndexRequest = request.getDestination.index() + + (searchRequestIndices + indexOfIndexRequest) + .flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: ReindexRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + val searchRequestIndices = actionRequest.getSearchRequest.indices().asSafeSet.flatMap(ClusterIndexName.fromString) + val isSearchRequestComposedOnlyOfAllowedIndices = (searchRequestIndices -- filteredIndices.toList).isEmpty + + val indexOfIndexRequest = actionRequest.getDestination.index() + val isDestinationIndexOnFilteredIndicesList = ClusterIndexName.fromString(indexOfIndexRequest).exists(filteredIndices.toList.contains(_)) + + if (isDestinationIndexOnFilteredIndicesList && isSearchRequestComposedOnlyOfAllowedIndices) { + Modified + } else { + if (!isDestinationIndexOnFilteredIndicesList) { + logger.info(s"[${id.show}] Destination index of _reindex request is forbidden") + } + if (!isSearchRequestComposedOnlyOfAllowedIndices) { + logger.info(s"[${id.show}] At least one index from sources indices list of _reindex request is forbidden") + } + ShouldBeInterrupted + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ResizeEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ResizeEsRequestContext.scala new file mode 100644 index 0000000000..d3458268ae --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ResizeEsRequestContext.scala @@ -0,0 +1,55 @@ +/* + * 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.admin.indices.shrink.ResizeRequest +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 + +import scala.jdk.CollectionConverters._ + +class ResizeEsRequestContext(actionRequest: ResizeRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ResizeRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ResizeRequest): Set[ClusterIndexName] = { + (request.getSourceIndex :: request.getTargetIndexRequest.index() :: request.getTargetIndexRequest.aliases().asScala.map(_.name()).toList) + .flatMap(ClusterIndexName.fromString) + .toSet + } + + override protected def update(request: ResizeRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + val notAllowedIndices = indicesFrom(actionRequest) -- filteredIndices.toList.toSet + if (notAllowedIndices.isEmpty) { + Modified + } else { + throw new IllegalStateException(s"Resize request is write request and such requests need all indices to be allowed. Not allowed indices=[${notAllowedIndices.map(_.show).mkString(",")}]") + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ResolveIndexEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ResolveIndexEsRequestContext.scala new file mode 100644 index 0000000000..8cb0c4d290 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ResolveIndexEsRequestContext.scala @@ -0,0 +1,121 @@ +/* + * 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 monix.eval.Task +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction.{ResolvedAlias, ResolvedIndex} +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect._ +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.domain +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 +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ + +class ResolveIndexEsRequestContext(actionRequest: ResolveIndexAction.Request, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[ResolveIndexAction.Request](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: ResolveIndexAction.Request): Set[domain.ClusterIndexName] = indicesOrWildcard { + request.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + } + + override protected def update(request: ResolveIndexAction.Request, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + request.indices(filteredIndices.toList.map(_.stringify): _*) + ModificationResult.UpdateResponse(resp => Task.delay(filterResponse(resp, allAllowedIndices))) + } + + override def modifyWhenIndexNotFound: ModificationResult = { + val randomNonexistingIndex = initialBlockContext.randomNonexistentIndex() + update(actionRequest, NonEmptyList.of(randomNonexistingIndex), NonEmptyList.of(randomNonexistingIndex)) + Modified + } + + private def filterResponse(response: ActionResponse, indices: NonEmptyList[ClusterIndexName]): ActionResponse = { + response match { + case r: ResolveIndexAction.Response => + new ResolveIndexAction.Response( + r.getIndices.asSafeList.flatMap(secureResolvedIndex(_, indices)).asJava, + r.getAliases.asSafeList.flatMap(secureResolvedAlias(_, indices)).asJava, + r.getDataStreams + ) + case r => r + } + } + + private def secureResolvedIndex(resolvedIndex: ResolvedIndex, allowedIndices: NonEmptyList[ClusterIndexName]) = { + if (isAllowed(resolvedIndex.getName, allowedIndices)) { + val allowedResolvedAliases = resolvedIndex + .getAliases.asSafeList + .filter(isAllowed(_, allowedIndices)) + Some(createResolvedIndex( + resolvedIndex.getName, + allowedResolvedAliases, + resolvedIndex.getAttributes, + resolvedIndex.getDataStream + )) + } else { + None + } + } + + private def secureResolvedAlias(resolvedAlias: ResolvedAlias, allowedIndices: NonEmptyList[ClusterIndexName]) = { + if (isAllowed(resolvedAlias.getName, allowedIndices)) { + val allowedResolvedIndices = resolvedAlias + .getIndices.asSafeList + .filter(isAllowed(_, allowedIndices)) + Some(createResolvedAlias( + resolvedAlias.getName, + allowedResolvedIndices + )) + } else { + None + } + } + + private def isAllowed(aliasOrIndex: String, allowedIndices: NonEmptyList[ClusterIndexName]) = { + val resolvedAliasOrIndexName = ClusterIndexName.Local.fromString(aliasOrIndex) + .getOrElse(throw new IllegalStateException(s"Cannot create IndexName from $aliasOrIndex")) + allowedIndices.exists(_.matches(resolvedAliasOrIndexName)) + } + + private def createResolvedIndex(index: String, aliases: List[String], attributes: Array[String], datastream: String) = { + onClass(classOf[ResolvedIndex]) + .create(index, aliases.toArray, attributes, datastream) + .get[ResolvedIndex]() + } + + private def createResolvedAlias(alias: String, indices: List[String]) = { + onClass(classOf[ResolvedAlias]) + .create(alias, indices.toArray) + .get[ResolvedAlias]() + } + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/RolloverEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/RolloverEsRequestContext.scala new file mode 100644 index 0000000000..3a6a90f6f9 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/RolloverEsRequestContext.scala @@ -0,0 +1,46 @@ +/* + * 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 org.elasticsearch.action.admin.indices.rollover.RolloverRequest +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 + +class RolloverEsRequestContext(actionRequest: RolloverRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseIndicesEsRequestContext[RolloverRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def indicesFrom(request: RolloverRequest): Set[ClusterIndexName] = { + (Option(request.getNewIndexName).toSet ++ Set(request.getRolloverTarget)) + .flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: RolloverRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/SearchEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/SearchEsRequestContext.scala new file mode 100644 index 0000000000..cff0b188e1 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/SearchEsRequestContext.scala @@ -0,0 +1,74 @@ +/* + * 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 org.elasticsearch.action.ActionResponse +import org.elasticsearch.action.search.{SearchRequest, SearchResponse} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.Strategy.BasedOnBlockContextOnly +import tech.beshu.ror.accesscontrol.domain._ +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.response.SearchHitOps._ +import tech.beshu.ror.es.handler.request.SearchRequestOps._ +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.utils.ScalaOps._ + +class SearchEsRequestContext(actionRequest: SearchRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override implicit val threadPool: ThreadPool) + extends BaseFilterableEsRequestContext[SearchRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def requestFieldsUsage: RequestFieldsUsage = actionRequest.checkFieldsUsage() + + override protected def indicesFrom(request: SearchRequest): Set[ClusterIndexName] = { + request.indices.asSafeSet.flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: SearchRequest, + indices: NonEmptyList[ClusterIndexName], + filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]): ModificationResult = { + request + .applyFilterToQuery(filter) + .applyFieldLevelSecurity(fieldLevelSecurity) + .indices(indices.toList.map(_.stringify): _*) + + ModificationResult.UpdateResponse.using(filterFieldsFromResponse(fieldLevelSecurity)) + } + + private def filterFieldsFromResponse(fieldLevelSecurity: Option[FieldLevelSecurity]) + (actionResponse: ActionResponse): ActionResponse = { + (actionResponse, fieldLevelSecurity) match { + case (response: SearchResponse, Some(FieldLevelSecurity(restrictions, _: BasedOnBlockContextOnly))) => + response.getHits.getHits + .foreach { hit => + hit + .filterSourceFieldsUsing(restrictions) + .filterDocumentFieldsUsing(restrictions) + } + response + case _ => + actionResponse + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/SqlIndicesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/SqlIndicesEsRequestContext.scala new file mode 100644 index 0000000000..a633f014fa --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/SqlIndicesEsRequestContext.scala @@ -0,0 +1,157 @@ +/* + * 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 monix.eval.Task +import org.elasticsearch.action.{ActionRequest, ActionResponse, CompositeIndicesRequest} +import org.elasticsearch.index.query.QueryBuilder +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect._ +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.Strategy.{BasedOnBlockContextOnly, FlsAtLuceneLevelApproach} +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, FieldLevelSecurity, Filter} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.{CannotModify, UpdateResponse} +import tech.beshu.ror.es.handler.response.FLSContextHeaderHandler +import tech.beshu.ror.es.utils.SqlRequestHelper +import tech.beshu.ror.exceptions.SecurityPermissionException + +class SqlIndicesEsRequestContext private(actionRequest: ActionRequest with CompositeIndicesRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseFilterableEsRequestContext[ActionRequest with CompositeIndicesRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + override protected def requestFieldsUsage: RequestFieldsUsage = RequestFieldsUsage.NotUsingFields + + private lazy val sqlIndicesExtractResult = SqlRequestHelper.indicesFrom(actionRequest) match { + case result@Right(_) => result + case result@Left(SqlRequestHelper.IndicesError.ParsingException) => result + case Left(SqlRequestHelper.IndicesError.UnexpectedException(ex)) => + throw RequestSeemsToBeInvalid[CompositeIndicesRequest](s"Cannot extract SQL indices from ${actionRequest.getClass.getName}", ex) + } + + override protected def indicesFrom(request: ActionRequest with CompositeIndicesRequest): Set[ClusterIndexName] = { + sqlIndicesExtractResult.map(_.indices.flatMap(ClusterIndexName.fromString)) match { + case Right(indices) => indices + case Left(_) => Set(ClusterIndexName.Local.wildcard) + } + } + + override protected def update(request: ActionRequest with CompositeIndicesRequest, + indices: NonEmptyList[ClusterIndexName], + filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]): ModificationResult = { + val result = for { + _ <- modifyRequestIndices(request, indices) + _ <- Right(applyFieldLevelSecurityTo(request, fieldLevelSecurity)) + _ <- Right(applyFilterTo(request, filter)) + } yield { + UpdateResponse { response => + Task.delay { + applyFieldLevelSecurityTo(response, fieldLevelSecurity) match { + case Right(modifiedResponse) => + modifiedResponse + case Left(SqlRequestHelper.ModificationError.UnexpectedException(ex)) => + throw new SecurityPermissionException("Cannot apply field level security to the SQL response", ex) + } + } + } + } + result.fold( + error => { + logger.error(s"[${id.show}] Cannot modify SQL indices of incoming request; error=$error") + CannotModify + }, + identity + ) + } + + private def modifyRequestIndices(request: ActionRequest with CompositeIndicesRequest, + indices: NonEmptyList[ClusterIndexName]): Either[SqlRequestHelper.ModificationError, CompositeIndicesRequest] = { + sqlIndicesExtractResult match { + case Right(sqlIndices) => + val indicesStrings = indices.map(_.stringify).toList.toSet + if (indicesStrings != sqlIndices.indices) { + SqlRequestHelper.modifyIndicesOf(request, sqlIndices, indicesStrings) + } else { + Right(request) + } + case Left(_) => + logger.debug(s"[${id.show}] Cannot parse SQL statement - we can pass it though, because ES is going to reject it") + Right(request) + } + } + + private def applyFieldLevelSecurityTo(request: ActionRequest with CompositeIndicesRequest, + fieldLevelSecurity: Option[FieldLevelSecurity]) = { + fieldLevelSecurity match { + case Some(definedFields) => + definedFields.strategy match { + case FlsAtLuceneLevelApproach => + FLSContextHeaderHandler.addContextHeader(threadPool, definedFields.restrictions, id) + request + case BasedOnBlockContextOnly.NotAllowedFieldsUsed(_) | BasedOnBlockContextOnly.EverythingAllowed => + request + } + case None => + request + } + } + + private def applyFieldLevelSecurityTo(response: ActionResponse, + fieldLevelSecurity: Option[FieldLevelSecurity]) = { + fieldLevelSecurity match { + case Some(fls) => + SqlRequestHelper.modifyResponseAccordingToFieldLevelSecurity(response, fls) + case None => + Right(response) + } + } + + private def applyFilterTo(request: ActionRequest with CompositeIndicesRequest, + filter: Option[Filter]) = { + import tech.beshu.ror.es.handler.request.SearchRequestOps._ + Option(on(request).call("filter").get[QueryBuilder]) + .wrapQueryBuilder(filter) + .foreach { qb => on(request).set("filter", qb) } + request + } +} + +object SqlIndicesEsRequestContext { + def unapply(arg: ReflectionBasedActionRequest): Option[SqlIndicesEsRequestContext] = { + if (arg.esContext.channel.request().path().startsWith("/_sql")) { + Some(new SqlIndicesEsRequestContext( + arg.esContext.actionRequest.asInstanceOf[ActionRequest with CompositeIndicesRequest], + arg.esContext, + arg.aclContext, + arg.clusterService, + arg.threadPool + )) + } else { + None + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/XpackAsyncSearchRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/XpackAsyncSearchRequestContext.scala new file mode 100644 index 0000000000..afaefac296 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/XpackAsyncSearchRequestContext.scala @@ -0,0 +1,104 @@ +/* + * 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 org.elasticsearch.action.search.{SearchRequest, SearchResponse} +import org.elasticsearch.action.{ActionRequest, ActionResponse} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, FieldLevelSecurity, IndexAttribute} +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.response.SearchHitOps._ +import tech.beshu.ror.es.handler.request.SearchRequestOps._ +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.utils.ReflecUtils.invokeMethodCached +import tech.beshu.ror.utils.ScalaOps._ + +class XpackAsyncSearchRequestContext private(actionRequest: ActionRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override implicit val threadPool: ThreadPool) + extends BaseFilterableEsRequestContext[ActionRequest](actionRequest, esContext, aclContext, clusterService, threadPool) { + + private lazy val searchRequest = searchRequestFrom(actionRequest) + + override lazy val indexAttributes: Set[IndexAttribute] = indexAttributesFrom(searchRequest) + + override protected def requestFieldsUsage: RequestFieldsUsage = searchRequest.checkFieldsUsage() + + override protected def indicesFrom(request: ActionRequest): Set[domain.ClusterIndexName] = { + searchRequest + .indices.asSafeSet + .flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: ActionRequest, + indices: NonEmptyList[domain.ClusterIndexName], + filter: Option[domain.Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]): ModificationResult = { + searchRequest + .applyFilterToQuery(filter) + .applyFieldLevelSecurity(fieldLevelSecurity) + .indices(indices.toList.map(_.stringify): _*) + + ModificationResult.UpdateResponse.using(filterFieldsFromResponse(fieldLevelSecurity)) + } + + private def searchRequestFrom(request: ActionRequest) = { + Option(invokeMethodCached(request, request.getClass, "getSearchRequest")) + .collect { case sr: SearchRequest => sr } + .getOrElse(throw new RequestSeemsToBeInvalid[ActionRequest]("Cannot extract SearchRequest from SubmitAsyncSearchRequest request")) + } + + private def filterFieldsFromResponse(fieldLevelSecurity: Option[FieldLevelSecurity]) + (actionResponse: ActionResponse): ActionResponse = { + (searchResponseFrom(actionResponse), fieldLevelSecurity) match { + case (Some(searchResponse), Some(definedFieldLevelSecurity)) => + searchResponse.getHits.getHits + .foreach { hit => + hit + .filterSourceFieldsUsing(definedFieldLevelSecurity.restrictions) + .filterDocumentFieldsUsing(definedFieldLevelSecurity.restrictions) + } + actionResponse + case _ => + actionResponse + } + } + + private def searchResponseFrom(response: ActionResponse) = { + Option(invokeMethodCached(response, response.getClass, "getSearchResponse")) + .collect { case sr: SearchResponse => sr } + } +} + +object XpackAsyncSearchRequestContext { + + def unapply(arg: ReflectionBasedActionRequest): Option[XpackAsyncSearchRequestContext] = { + if (arg.esContext.actionRequest.getClass.getSimpleName.startsWith("SubmitAsyncSearchRequest")) { + Some(new XpackAsyncSearchRequestContext(arg.esContext.actionRequest, arg.esContext, arg.aclContext, arg.clusterService, arg.threadPool)) + } else { + None + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala new file mode 100644 index 0000000000..af072d1161 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/CreateDataStreamEsRequestContext.scala @@ -0,0 +1,46 @@ +/* + * 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 org.elasticsearch.action.datastreams.CreateDataStreamAction +import org.elasticsearch.threadpool.ThreadPool +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.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.BaseDataStreamsEsRequestContext + +class CreateDataStreamEsRequestContext(actionRequest: CreateDataStreamAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: CreateDataStreamAction.Request): Set[domain.DataStreamName] = originDataStreams + + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala new file mode 100644 index 0000000000..913c440f62 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DataStreamsStatsEsRequestContext.scala @@ -0,0 +1,46 @@ +/* + * 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 org.elasticsearch.action.datastreams.DataStreamsStatsAction +import org.elasticsearch.threadpool.ThreadPool +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.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.BaseDataStreamsEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class DataStreamsStatsEsRequestContext(actionRequest: DataStreamsStatsAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override def backingIndicesFrom(request: DataStreamsStatsAction.Request): BackingIndices = BackingIndices.IndicesNotInvolved + + override def dataStreamsFrom(request: DataStreamsStatsAction.Request): Set[domain.DataStreamName] = + actionRequest.indices().asSafeList.flatMap(DataStreamName.fromString).toSet + + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala new file mode 100644 index 0000000000..856181f959 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/DeleteDataStreamEsRequestContext.scala @@ -0,0 +1,50 @@ +/* + * 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 org.elasticsearch.action.datastreams.DeleteDataStreamAction +import org.elasticsearch.threadpool.ThreadPool +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 +import tech.beshu.ror.es.handler.request.context.types.BaseDataStreamsEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class DeleteDataStreamEsRequestContext(actionRequest: DeleteDataStreamAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + 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 protected def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + setDataStreamNames(blockContext.dataStreams) + ModificationResult.Modified + } + + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala new file mode 100644 index 0000000000..b587e943c3 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/GetDataStreamEsRequestContext.scala @@ -0,0 +1,104 @@ +/* + * 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 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.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.DataStreamRequestBlockContext.BackingIndices +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DataStreamName} +import tech.beshu.ror.accesscontrol.matchers.PatternsMatcher +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.BaseDataStreamsEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ + +class GetDataStreamEsRequestContext(actionRequest: GetDataStreamAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: 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(_, allAllowedIndices) => + Task.now(updateGetDataStreamResponse(r, extendAllowedIndicesSet(allAllowedIndices))) + case BackingIndices.IndicesNotInvolved => + Task.now(r) + } + case r => + Task.now(r) + } + } + + private def extendAllowedIndicesSet(allowedIndices: Iterable[ClusterIndexName]) = { + allowedIndices.toList.map(_.formatAsDataStreamBackingIndexName).toSet ++ allowedIndices.toSet + } + + private def setDataStreamNames(dataStreams: Set[domain.DataStreamName]): Unit = { + actionRequest.indices(dataStreams.map(DataStreamName.toString).toList: _*) // method is named indices but it sets data streams + } + + private def updateGetDataStreamResponse(response: GetDataStreamAction.Response, + allAllowedIndices: Iterable[ClusterIndexName]): GetDataStreamAction.Response = { + val allowedIndicesMatcher = PatternsMatcher.create(allAllowedIndices) + val filteredStreams = + response + .getDataStreams.asSafeList + .filter { dataStreamInfo: Response.DataStreamInfo => + backingIndiesMatchesAllowedIndices(dataStreamInfo, allowedIndicesMatcher) + } + new GetDataStreamAction.Response(filteredStreams.asJava) + } + + private def backingIndiesMatchesAllowedIndices(info: Response.DataStreamInfo, allowedIndicesMatcher: PatternsMatcher[ClusterIndexName]) = { + val dataStreamIndices: Set[ClusterIndexName] = indicesFrom(info).keySet + val allowedBackingIndices = allowedIndicesMatcher.filter(dataStreamIndices) + dataStreamIndices.diff(allowedBackingIndices).isEmpty + } + + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala new file mode 100644 index 0000000000..90c92d7adb --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/MigrateToDataStreamEsRequestContext.scala @@ -0,0 +1,47 @@ +/* + * 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 org.elasticsearch.action.datastreams.MigrateToDataStreamAction +import org.elasticsearch.threadpool.ThreadPool +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.es.handler.request.context.types.BaseDataStreamsEsRequestContext + +class MigrateToDataStreamEsRequestContext(actionRequest: MigrateToDataStreamAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originIndices = Option(actionRequest.getAliasName).flatMap(ClusterIndexName.fromString).toSet + + override protected def dataStreamsFrom(request: MigrateToDataStreamAction.Request): Set[domain.DataStreamName] = Set.empty + + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala new file mode 100644 index 0000000000..8d0a80fcc1 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/ModifyDataStreamsEsRequestContext.scala @@ -0,0 +1,52 @@ +/* + * 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 org.elasticsearch.action.datastreams.ModifyDataStreamsAction +import org.elasticsearch.threadpool.ThreadPool +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, 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.BaseDataStreamsEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class ModifyDataStreamsEsRequestContext(actionRequest: ModifyDataStreamsAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originIndices: Set[domain.ClusterIndexName] = { + actionRequest.getActions.asSafeList.map(_.getIndex).flatMap(ClusterIndexName.fromString).toSet + } + + 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 + } + + override def modifyRequest(blockContext: BlockContext.DataStreamRequestBlockContext): ModificationResult = { + ModificationResult.Modified // data stream and indices already processed by ACL + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala new file mode 100644 index 0000000000..91a9ece7f1 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/datastreams/PromoteDataStreamEsRequestContext.scala @@ -0,0 +1,46 @@ +/* + * 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 org.elasticsearch.action.datastreams.PromoteDataStreamAction +import org.elasticsearch.threadpool.ThreadPool +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.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.BaseDataStreamsEsRequestContext + +class PromoteDataStreamEsRequestContext(actionRequest: PromoteDataStreamAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseDataStreamsEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + private lazy val originDataStreams = Option(actionRequest.getName).flatMap(DataStreamName.fromString).toSet + + override protected def dataStreamsFrom(request: PromoteDataStreamAction.Request): Set[domain.DataStreamName] = + originDataStreams + + 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/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/CleanupRepositoryEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/CleanupRepositoryEsRequestContext.scala new file mode 100644 index 0000000000..c92aed49f1 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/CleanupRepositoryEsRequestContext.scala @@ -0,0 +1,51 @@ +/* + * 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.repositories + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.admin.cluster.repositories.cleanup.CleanupRepositoryRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.domain.RepositoryName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.types.BaseRepositoriesEsRequestContext + +class CleanupRepositoryEsRequestContext(actionRequest: CleanupRepositoryRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseRepositoriesEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def repositoriesFrom(request: CleanupRepositoryRequest): Set[RepositoryName] = Set { + RepositoryName + .from(request.name()) + .getOrElse(throw RequestSeemsToBeInvalid[CleanupRepositoryRequest]("Repository name is empty")) + } + + override protected def update(request: CleanupRepositoryRequest, + repositories: NonEmptyList[RepositoryName]): ModificationResult = { + if (repositories.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.toList.mkString(",")}]") + } + request.name(RepositoryName.toString(repositories.head)) + Modified + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/CreateRepositoryEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/CreateRepositoryEsRequestContext.scala new file mode 100644 index 0000000000..5d0c95fecb --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/CreateRepositoryEsRequestContext.scala @@ -0,0 +1,51 @@ +/* + * 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.repositories + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.domain.RepositoryName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.types.BaseRepositoriesEsRequestContext + +class CreateRepositoryEsRequestContext(actionRequest: PutRepositoryRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseRepositoriesEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def repositoriesFrom(request: PutRepositoryRequest): Set[RepositoryName] = Set { + RepositoryName + .from(request.name()) + .getOrElse(throw RequestSeemsToBeInvalid[PutRepositoryRequest]("Repository name is empty")) + } + + override protected def update(request: PutRepositoryRequest, + repositories: NonEmptyList[RepositoryName]): ModificationResult = { + if (repositories.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.toList.mkString(",")}]") + } + request.name(RepositoryName.toString(repositories.head)) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/DeleteRepositoryEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/DeleteRepositoryEsRequestContext.scala new file mode 100644 index 0000000000..b0949e6f1e --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/DeleteRepositoryEsRequestContext.scala @@ -0,0 +1,51 @@ +/* + * 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.repositories + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.domain.RepositoryName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.types.BaseRepositoriesEsRequestContext + +class DeleteRepositoryEsRequestContext(actionRequest: DeleteRepositoryRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseRepositoriesEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def repositoriesFrom(request: DeleteRepositoryRequest): Set[RepositoryName] = Set { + RepositoryName + .from(request.name()) + .getOrElse(throw RequestSeemsToBeInvalid[DeleteRepositoryRequest]("Repository name is empty")) + } + + override protected def update(request: DeleteRepositoryRequest, + repositories: NonEmptyList[RepositoryName]): ModificationResult = { + if (repositories.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.toList.mkString(",")}]") + } + request.name(RepositoryName.toString(repositories.head)) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/GetRepositoriesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/GetRepositoriesEsRequestContext.scala new file mode 100644 index 0000000000..a0c5a78814 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/GetRepositoriesEsRequestContext.scala @@ -0,0 +1,47 @@ +/* + * 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.repositories + +import cats.data.NonEmptyList +import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.domain.RepositoryName +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 +import tech.beshu.ror.es.handler.request.context.types.BaseRepositoriesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class GetRepositoriesEsRequestContext(actionRequest: GetRepositoriesRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseRepositoriesEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def repositoriesFrom(request: GetRepositoriesRequest): Set[RepositoryName] = { + repositoriesOrWildcard { + request.repositories().asSafeSet.flatMap(RepositoryName.from) + } + } + + override protected def update(request: GetRepositoriesRequest, + repositories: NonEmptyList[RepositoryName]): ModificationResult = { + request.repositories(repositories.map(RepositoryName.toString).toList.toArray) + Modified + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/VerifyRepositoryEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/VerifyRepositoryEsRequestContext.scala new file mode 100644 index 0000000000..51e8302f3f --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/repositories/VerifyRepositoryEsRequestContext.scala @@ -0,0 +1,51 @@ +/* + * 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.repositories + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.domain.RepositoryName +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.types.BaseRepositoriesEsRequestContext + +class VerifyRepositoryEsRequestContext(actionRequest: VerifyRepositoryRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseRepositoriesEsRequestContext(actionRequest, esContext, clusterService, threadPool) { + + override protected def repositoriesFrom(request: VerifyRepositoryRequest): Set[RepositoryName] = Set { + RepositoryName + .from(request.name()) + .getOrElse(throw RequestSeemsToBeInvalid[VerifyRepositoryRequest]("Repository name is empty")) + } + + override protected def update(request: VerifyRepositoryRequest, + repositories: NonEmptyList[RepositoryName]): ModificationResult = { + if (repositories.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.toList.mkString(",")}]") + } + request.name(RepositoryName.toString(repositories.head)) + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/AuditEventESRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/AuditEventESRequestContext.scala new file mode 100644 index 0000000000..9ca0525287 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/AuditEventESRequestContext.scala @@ -0,0 +1,47 @@ +/* + * 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.ror + +import org.elasticsearch.threadpool.ThreadPool +import org.json.JSONObject +import tech.beshu.ror.accesscontrol.blocks.BlockContext.GeneralNonIndexRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.actions.rrauditevent.RRAuditEventRequest +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +class AuditEventESRequestContext(actionRequest: RRAuditEventRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[GeneralNonIndexRequestBlockContext](esContext, clusterService) + with EsRequest[GeneralNonIndexRequestBlockContext] { + + override val initialBlockContext: GeneralNonIndexRequestBlockContext = GeneralNonIndexRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty + ) + + override val generalAuditEvents: JSONObject = actionRequest.auditEvents + + override protected def modifyRequest(blockContext: GeneralNonIndexRequestBlockContext): ModificationResult = Modified +} + diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/CurrentUserMetadataEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/CurrentUserMetadataEsRequestContext.scala new file mode 100644 index 0000000000..e3ca8b2d16 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/CurrentUserMetadataEsRequestContext.scala @@ -0,0 +1,47 @@ +/* + * 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.ror + +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.CurrentUserMetadataRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.actions.rrmetadata.RRUserMetadataRequest +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +import scala.annotation.nowarn + +class CurrentUserMetadataEsRequestContext(@nowarn("cat=unused") actionRequest: RRUserMetadataRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[CurrentUserMetadataRequestBlockContext](esContext, clusterService) + with EsRequest[CurrentUserMetadataRequestBlockContext] { + + override lazy val isReadOnlyRequest: Boolean = true + + override val initialBlockContext: CurrentUserMetadataRequestBlockContext = CurrentUserMetadataRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty + ) + + override protected def modifyRequest(blockContext: CurrentUserMetadataRequestBlockContext): ModificationResult = Modified +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/RorApiEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/RorApiEsRequestContext.scala new file mode 100644 index 0000000000..f614066195 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/ror/RorApiEsRequestContext.scala @@ -0,0 +1,49 @@ +/* + * 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.ror + +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.RorApiRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.actions.RorActionRequest +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} + +class RorApiEsRequestContext(actionRequest: RorActionRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseEsRequestContext[RorApiRequestBlockContext](esContext, clusterService) + with EsRequest[RorApiRequestBlockContext] { + + override val initialBlockContext: RorApiRequestBlockContext = RorApiRequestBlockContext( + requestContext = this, + userMetadata = UserMetadata.from(this), + responseHeaders = Set.empty, + responseTransformations = List.empty + ) + + override protected def modifyRequest(blockContext: RorApiRequestBlockContext): ModificationResult = { + blockContext.userMetadata.loggedUser match { + case Some(value) => actionRequest.setLoggedUser(value) + case None => + } + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/CreateSnapshotEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/CreateSnapshotEsRequestContext.scala new file mode 100644 index 0000000000..dcca460541 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/CreateSnapshotEsRequestContext.scala @@ -0,0 +1,112 @@ +/* + * 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.snapshots + +import cats.implicits._ +import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.SnapshotRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseSnapshotEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.jdk.CollectionConverters._ + +class CreateSnapshotEsRequestContext(actionRequest: CreateSnapshotRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSnapshotEsRequestContext[CreateSnapshotRequest](actionRequest, esContext, clusterService, threadPool) { + + override def snapshotsFrom(request: CreateSnapshotRequest): Set[SnapshotName] = Set { + SnapshotName + .from(request.snapshot()) + .getOrElse(throw RequestSeemsToBeInvalid[CreateSnapshotRequest]("Snapshot name is empty")) + } + + override protected def repositoriesFrom(request: CreateSnapshotRequest): Set[RepositoryName] = Set { + RepositoryName + .from(request.repository()) + .getOrElse(throw RequestSeemsToBeInvalid[CreateSnapshotRequest]("Repository name is empty")) + } + + override protected def indicesFrom(request: CreateSnapshotRequest): Set[ClusterIndexName] = { + indicesOrWildcard(request.indices().asSafeSet.flatMap(ClusterIndexName.fromString)) + } + + override protected def modifyRequest(blockContext: SnapshotRequestBlockContext): ModificationResult = { + val updateResult = for { + snapshot <- snapshotFrom(blockContext) + repository <- repositoryFrom(blockContext) + indices <- indicesFrom(blockContext) + } yield update(actionRequest, snapshot, repository, indices) + updateResult match { + case Right(_) => + ModificationResult.Modified + case Left(_) => + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request. It's safer to forbid the request, but it looks like an issue. Please, report it as soon as possible.") + ModificationResult.ShouldBeInterrupted + } + } + + private def snapshotFrom(blockContext: SnapshotRequestBlockContext) = { + val snapshots = blockContext.snapshots.toList + snapshots match { + case Nil => + Left(()) + case snapshot :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${blockContext.requestContext.id.show}] Filtered result contains more than one snapshot. First was taken. The whole set of snapshots [${snapshots.mkString(",")}]") + } + Right(snapshot) + } + } + + private def repositoryFrom(blockContext: SnapshotRequestBlockContext) = { + val repositories = blockContext.repositories.toList + repositories match { + case Nil => + Left(()) + case repository :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${blockContext.requestContext.id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.mkString(",")}]") + } + Right(repository) + } + } + + private def indicesFrom(blockContext: SnapshotRequestBlockContext) = { + UniqueNonEmptyList.fromIterable(blockContext.filteredIndices) match { + case Some(value) => Right(value) + case None => Left(()) + } + } + + private def update(actionRequest: CreateSnapshotRequest, + snapshot: SnapshotName, + repository: RepositoryName, + indices: UniqueNonEmptyList[ClusterIndexName]) = { + actionRequest.snapshot(SnapshotName.toString(snapshot)) + actionRequest.repository(RepositoryName.toString(repository)) + actionRequest.indices(indices.toList.map(_.stringify).asJava) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/DeleteSnapshotEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/DeleteSnapshotEsRequestContext.scala new file mode 100644 index 0000000000..d4682da5cb --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/DeleteSnapshotEsRequestContext.scala @@ -0,0 +1,91 @@ +/* + * 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.snapshots + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.SnapshotRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseSnapshotEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class DeleteSnapshotEsRequestContext(actionRequest: DeleteSnapshotRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSnapshotEsRequestContext[DeleteSnapshotRequest](actionRequest, esContext, clusterService, threadPool) { + + override protected def snapshotsFrom(request: DeleteSnapshotRequest): Set[SnapshotName] = { + request + .snapshots().asSafeSet + .flatMap(SnapshotName.from) + } + + override protected def repositoriesFrom(request: DeleteSnapshotRequest): Set[RepositoryName] = Set { + RepositoryName + .from(request.repository()) + .getOrElse(throw RequestSeemsToBeInvalid[DeleteSnapshotRequest]("Repository name is empty")) + } + + override protected def indicesFrom(request: DeleteSnapshotRequest): Set[ClusterIndexName] = Set.empty + + override protected def modifyRequest(blockContext: SnapshotRequestBlockContext): ModificationResult = { + val updateResult = for { + snapshots <- snapshotsFrom(blockContext) + repository <- repositoryFrom(blockContext) + } yield update(actionRequest, snapshots, repository) + updateResult match { + case Right(_) => + ModificationResult.Modified + case Left(_) => + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request. It's safer to forbid the request, but it looks like an issue. Please, report it as soon as possible.") + ModificationResult.ShouldBeInterrupted + } + } + + private def snapshotsFrom(blockContext: SnapshotRequestBlockContext) = { + NonEmptyList + .fromList(blockContext.snapshots.toList) + .toRight(()) + } + + private def repositoryFrom(blockContext: SnapshotRequestBlockContext) = { + val repositories = blockContext.repositories.toList + repositories match { + case Nil => + Left(()) + case repository :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${blockContext.requestContext.id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.mkString(",")}]") + } + Right(repository) + } + } + + private def update(actionRequest: DeleteSnapshotRequest, + snapshots: NonEmptyList[SnapshotName], + repository: RepositoryName) = { + actionRequest.snapshots(snapshots.toList.map(SnapshotName.toString): _*) + actionRequest.repository(RepositoryName.toString(repository)) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/GetSnapshotsEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/GetSnapshotsEsRequestContext.scala new file mode 100644 index 0000000000..27844d18a8 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/GetSnapshotsEsRequestContext.scala @@ -0,0 +1,114 @@ +/* + * 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.snapshots + +import cats.implicits._ +import monix.eval.Task +import org.elasticsearch.action.admin.cluster.snapshots.get.{GetSnapshotsRequest, GetSnapshotsResponse} +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect.on +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.SnapshotRequestBlockContext +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +import tech.beshu.ror.accesscontrol.matchers.PatternsMatcher +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.BaseSnapshotEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.jdk.CollectionConverters._ + +class GetSnapshotsEsRequestContext(actionRequest: GetSnapshotsRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSnapshotEsRequestContext[GetSnapshotsRequest](actionRequest, esContext, clusterService, threadPool) { + + override protected def snapshotsFrom(request: GetSnapshotsRequest): Set[SnapshotName] = { + request + .snapshots().asSafeList + .flatMap(SnapshotName.from) + .toSet[SnapshotName] + } + + override protected def repositoriesFrom(request: GetSnapshotsRequest): Set[RepositoryName] = { + request + .repositories().asSafeList + .flatMap(RepositoryName.from) + .toSet + } + + override protected def indicesFrom(request: GetSnapshotsRequest): Set[domain.ClusterIndexName] = + Set(ClusterIndexName.Local.wildcard) + + override protected def modifyRequest(blockContext: BlockContext.SnapshotRequestBlockContext): ModificationResult = { + val updateResult = for { + snapshots <- snapshotsFrom(blockContext) + repository <- repositoriesFrom(blockContext) + } yield update(actionRequest, snapshots, repository) + updateResult match { + case Right(_) => + ModificationResult.UpdateResponse { + case r: GetSnapshotsResponse => + Task.now(updateGetSnapshotResponse(r, blockContext.allAllowedIndices)) + case r => + Task.now(r) + } + case Left(_) => + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request. It's safer to forbid the request, but it looks like an issue. Please, report it as soon as possible.") + ModificationResult.ShouldBeInterrupted + } + } + + private def snapshotsFrom(blockContext: SnapshotRequestBlockContext) = { + UniqueNonEmptyList.fromIterable(blockContext.snapshots) match { + case Some(list) => Right(list) + case None => Left(()) + } + } + + private def repositoriesFrom(blockContext: SnapshotRequestBlockContext) = { + UniqueNonEmptyList.fromIterable(blockContext.repositories) match { + case Some(list) => Right(list) + case None => Left(()) + } + } + + private def update(actionRequest: GetSnapshotsRequest, + snapshots: UniqueNonEmptyList[SnapshotName], + repositories: UniqueNonEmptyList[RepositoryName]) = { + actionRequest.snapshots(snapshots.toList.map(SnapshotName.toString).toArray) + actionRequest.repositories(repositories.toList.map(RepositoryName.toString).toArray: _*) + } + + private def updateGetSnapshotResponse(response: GetSnapshotsResponse, + allAllowedIndices: Set[ClusterIndexName]): GetSnapshotsResponse = { + val matcher = PatternsMatcher.create(allAllowedIndices) + response + .getSnapshots.asSafeList + .foreach { snapshot => + val snapshotIndices = snapshot.indices().asSafeList.flatMap(ClusterIndexName.fromString).toSet + val filteredSnapshotIndices = matcher.filter(snapshotIndices).map(_.stringify).toList.asJava + on(snapshot).set("indices", filteredSnapshotIndices) + snapshot + } + response + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/RestoreSnapshotEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/RestoreSnapshotEsRequestContext.scala new file mode 100644 index 0000000000..53123097f2 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/RestoreSnapshotEsRequestContext.scala @@ -0,0 +1,110 @@ +/* + * 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.snapshots + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.SnapshotRequestBlockContext +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseSnapshotEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class RestoreSnapshotEsRequestContext(actionRequest: RestoreSnapshotRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSnapshotEsRequestContext[RestoreSnapshotRequest](actionRequest, esContext, clusterService, threadPool) { + + override protected def snapshotsFrom(request: RestoreSnapshotRequest): Set[domain.SnapshotName] = + SnapshotName + .from(request.snapshot()) + .toSet + + override protected def repositoriesFrom(request: RestoreSnapshotRequest): Set[domain.RepositoryName] = Set { + RepositoryName + .from(request.repository()) + .getOrElse(throw RequestSeemsToBeInvalid[RestoreSnapshotRequest]("Repository name is empty")) + } + + override protected def indicesFrom(request: RestoreSnapshotRequest): Set[domain.ClusterIndexName] = + indicesOrWildcard(request.indices.asSafeSet.flatMap(ClusterIndexName.fromString)) + + override protected def modifyRequest(blockContext: BlockContext.SnapshotRequestBlockContext): ModificationResult = { + val updateResult = for { + snapshots <- snapshotFrom(blockContext) + repository <- repositoryFrom(blockContext) + indices <- indicesFrom(blockContext) + } yield update(actionRequest, snapshots, repository, indices) + updateResult match { + case Right(_) => + ModificationResult.Modified + case Left(_) => + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request. It's safer to forbid the request, but it looks like an issue. Please, report it as soon as possible.") + ModificationResult.ShouldBeInterrupted + } + } + + private def snapshotFrom(blockContext: SnapshotRequestBlockContext) = { + val snapshots = blockContext.snapshots.toList + snapshots match { + case Nil => + Left(()) + case snapshot :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${blockContext.requestContext.id.show}] Filtered result contains more than one snapshot. First was taken. The whole set of repositories [${snapshots.mkString(",")}]") + } + Right(snapshot) + } + } + + private def repositoryFrom(blockContext: SnapshotRequestBlockContext) = { + val repositories = blockContext.repositories.toList + repositories match { + case Nil => + Left(()) + case repository :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${blockContext.requestContext.id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.mkString(",")}]") + } + Right(repository) + } + } + + private def indicesFrom(blockContext: SnapshotRequestBlockContext) = { + NonEmptyList.fromList(blockContext.filteredIndices.toList) match { + case None => Left(()) + case Some(indices) => Right(indices) + } + } + + private def update(request: RestoreSnapshotRequest, + snapshot: SnapshotName, + repository: RepositoryName, + indices: NonEmptyList[ClusterIndexName]) = { + request.snapshot(SnapshotName.toString(snapshot)) + request.repository(RepositoryName.toString(repository)) + request.indices(indices.toList.map(_.stringify): _*) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/SnapshotsStatusEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/SnapshotsStatusEsRequestContext.scala new file mode 100644 index 0000000000..9722e80665 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/snapshots/SnapshotsStatusEsRequestContext.scala @@ -0,0 +1,151 @@ +/* + * 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.snapshots + +import cats.data.NonEmptyList +import cats.implicits._ +import monix.eval.Task +import org.elasticsearch.action.admin.cluster.snapshots.status.{SnapshotsStatusRequest, SnapshotsStatusResponse} +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect.on +import tech.beshu.ror.accesscontrol.blocks.BlockContext.SnapshotRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, RepositoryName, SnapshotName} +import tech.beshu.ror.accesscontrol.matchers.PatternsMatcher +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseSnapshotEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ + +class SnapshotsStatusEsRequestContext(actionRequest: SnapshotsStatusRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseSnapshotEsRequestContext[SnapshotsStatusRequest](actionRequest, esContext, clusterService, threadPool) { + + override protected def snapshotsFrom(request: SnapshotsStatusRequest): Set[SnapshotName] = + request + .snapshots().asSafeList + .flatMap(SnapshotName.from) + .toSet[SnapshotName] + + override protected def repositoriesFrom(request: SnapshotsStatusRequest): Set[RepositoryName] = Set { + RepositoryName + .from(request.repository()) + .getOrElse(throw RequestSeemsToBeInvalid[SnapshotsStatusRequest]("Repository name is empty")) + } + + override protected def indicesFrom(request: SnapshotsStatusRequest): Set[ClusterIndexName] = + Set(ClusterIndexName.Local.wildcard) + + override protected def modifyRequest(blockContext: SnapshotRequestBlockContext): ModificationResult = { + if (isCurrentSnapshotStatusRequest(actionRequest)) updateSnapshotStatusResponse(blockContext) + else modifySnapshotStatusRequest(actionRequest, blockContext) + } + + private def updateSnapshotStatusResponse(blockContext: SnapshotRequestBlockContext): ModificationResult = { + ModificationResult.UpdateResponse { + case r: SnapshotsStatusResponse => Task.delay(filterOutNotAllowedSnapshotsAndRepositories(r, blockContext)) + case r => Task.now(r) + } + } + + private def filterOutNotAllowedSnapshotsAndRepositories(response: SnapshotsStatusResponse, + blockContext: SnapshotRequestBlockContext): SnapshotsStatusResponse = { + val allowedRepositoriesMatcher = PatternsMatcher.create(blockContext.repositories) + val allowedSnapshotsMatcher = PatternsMatcher.create(blockContext.snapshots) + + val allowedSnapshotStatuses = response + .getSnapshots.asSafeList + .filter { snapshotStatus => + (for { + repositoryName <- RepositoryName.from(snapshotStatus.getSnapshot.getRepository) + snapshotName <- SnapshotName.from(snapshotStatus.getSnapshot.getSnapshotId.getName) + } yield { + allowedRepositoriesMatcher.`match`(repositoryName) && + allowedSnapshotsMatcher.`match`(snapshotName) + }) getOrElse false + } + + on(response).set("snapshots", allowedSnapshotStatuses.asJava) + response + } + + private def modifySnapshotStatusRequest(request: SnapshotsStatusRequest, + blockContext: SnapshotRequestBlockContext) = { + val updateResult = for { + snapshots <- snapshotsFrom(blockContext) + repository <- repositoryFrom(blockContext) + } yield update(request, snapshots, repository) + updateResult match { + case Right(_) => + ModificationResult.Modified + case Left(_) => + logger.error(s"[${id.show}] Cannot update ${actionRequest.getClass.getSimpleName} request. It's safer to forbid the request, but it looks like an issue. Please, report it as soon as possible.") + ModificationResult.ShouldBeInterrupted + } + } + + private def snapshotsFrom(blockContext: SnapshotRequestBlockContext) = { + NonEmptyList.fromList(fullNamedSnapshotsFrom(blockContext.snapshots).toList) match { + case Some(list) => Right(list) + case None => Left(()) + } + } + + private def fullNamedSnapshotsFrom(snapshots: Iterable[SnapshotName]): Set[SnapshotName.Full] = { + val allFullNameSnapshots: Set[SnapshotName.Full] = allSnapshots.values.toSet.flatten + PatternsMatcher + .create(snapshots) + .filter(allFullNameSnapshots) + } + + private def repositoryFrom(blockContext: SnapshotRequestBlockContext) = { + val repositories = fullNamedRepositoriesFrom(blockContext.repositories).toList + repositories match { + case Nil => + Left(()) + case repository :: rest => + if (rest.nonEmpty) { + logger.warn(s"[${blockContext.requestContext.id.show}] Filtered result contains more than one repository. First was taken. The whole set of repositories [${repositories.mkString(",")}]") + } + Right(repository) + } + } + + private def fullNamedRepositoriesFrom(repositories: Iterable[RepositoryName]): Set[RepositoryName.Full] = { + val allFullNameRepositories: Set[RepositoryName.Full] = allSnapshots.keys.toSet + PatternsMatcher + .create(repositories) + .filter(allFullNameRepositories) + } + + private def update(actionRequest: SnapshotsStatusRequest, + snapshots: NonEmptyList[SnapshotName.Full], + repository: RepositoryName.Full) = { + actionRequest.snapshots(snapshots.toList.map(SnapshotName.toString).toArray) + actionRequest.repository(RepositoryName.toString(repository)) + } + + private def isCurrentSnapshotStatusRequest(actionRequest: SnapshotsStatusRequest) = { + val repositories = repositoriesFrom(actionRequest) + (repositories.isEmpty || repositories == Set(RepositoryName.all)) && snapshotsFrom(actionRequest).isEmpty + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteComponentTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteComponentTemplateEsRequestContext.scala new file mode 100644 index 0000000000..8a38020ce0 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteComponentTemplateEsRequestContext.scala @@ -0,0 +1,107 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.Version +import org.elasticsearch.action.admin.indices.template.delete.{TransportDeleteComponentTemplateAction, DeleteIndexTemplateRequest} +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect.on +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.TemplateNamePattern +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.DeletingComponentTemplates +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class DeleteComponentTemplateEsRequestContext(actionRequest: TransportDeleteComponentTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[TransportDeleteComponentTemplateAction.Request, DeletingComponentTemplates]( + actionRequest, esContext, clusterService, threadPool + ) { + + override protected def templateOperationFrom(request: TransportDeleteComponentTemplateAction.Request): DeletingComponentTemplates = { + NonEmptyList.fromList(request.getNames) match { + case Some(patterns) => DeletingComponentTemplates(patterns) + case None => throw RequestSeemsToBeInvalid[DeleteIndexTemplateRequest]("No template name patterns found") + } + } + + override protected def modifyRequest(blockContext: TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case DeletingComponentTemplates(namePatterns) => + actionRequest.updateNames(namePatterns) + ModificationResult.Modified + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } + + implicit class TransportDeleteComponentTemplateActionRequestOps(request: TransportDeleteComponentTemplateAction.Request) { + + def getNames: List[TemplateNamePattern] = { + if (isEsNewerThan712) getNamesForEsPost12 + else getNamesForEsPre13 + } + + def updateNames(names: NonEmptyList[TemplateNamePattern]): Unit = { + if (isEsNewerThan712) updateNamesForEsPost12(names) + else updateNamesForEsPre13(names) + } + + private def isEsNewerThan712 = { + Version.CURRENT.after(Version.fromString("7.12.1")) + } + + private def getNamesForEsPre13 = { + Option(on(request).call("name").get[String]).toList + .flatMap(TemplateNamePattern.fromString) + } + + private def getNamesForEsPost12 = { + on(request) + .call("names") + .get[Array[String]] + .asSafeList + .flatMap(TemplateNamePattern.fromString) + } + + private def updateNamesForEsPre13(names: NonEmptyList[TemplateNamePattern]) = { + names.tail match { + case Nil => + case _ => + logger.warn( + s"""[${id.show}] Filtered result contains more than one template pattern. First was taken. + | The whole set of patterns [${names.toList.mkString(",")}]""".oneLiner) + } + on(request).call("name", names.head.value.value) + } + + private def updateNamesForEsPost12(names: NonEmptyList[TemplateNamePattern]): Unit = { + on(request).set("names", names.toList.map(_.value.value).toArray) + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteComposableIndexTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteComposableIndexTemplateEsRequestContext.scala new file mode 100644 index 0000000000..4e85fd3e29 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteComposableIndexTemplateEsRequestContext.scala @@ -0,0 +1,107 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.Version +import org.elasticsearch.action.admin.indices.template.delete.{DeleteIndexTemplateRequest, TransportDeleteComposableIndexTemplateAction} +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect.on +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.TemplateNamePattern +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.DeletingIndexTemplates +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class DeleteComposableIndexTemplateEsRequestContext(actionRequest: TransportDeleteComposableIndexTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[TransportDeleteComposableIndexTemplateAction.Request, DeletingIndexTemplates]( + actionRequest, esContext, clusterService, threadPool + ) { + + override protected def templateOperationFrom(request: TransportDeleteComposableIndexTemplateAction.Request): DeletingIndexTemplates = { + NonEmptyList.fromList(request.getNames) match { + case Some(patterns) => DeletingIndexTemplates(patterns) + case None => throw RequestSeemsToBeInvalid[DeleteIndexTemplateRequest]("No template name patterns found") + } + } + + override protected def modifyRequest(blockContext: TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case DeletingIndexTemplates(namePatterns) => + actionRequest.updateNames(namePatterns) + ModificationResult.Modified + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } + + implicit class DeleteComposableIndexTemplateActionRequestOps(request: TransportDeleteComposableIndexTemplateAction.Request) { + + def getNames: List[TemplateNamePattern] = { + if (isEsNewerThan712) getNamesForEsPost12 + else getNamesForEsPre13 + } + + def updateNames(names: NonEmptyList[TemplateNamePattern]): Unit = { + if (isEsNewerThan712) updateNamesForEsPost12(names) + else updateNamesForEsPre13(names) + } + + private def isEsNewerThan712 = { + Version.CURRENT.after(Version.fromString("7.12.1")) + } + + private def getNamesForEsPre13 = { + Option(on(request).call("name").get[String]).toList + .flatMap(TemplateNamePattern.fromString) + } + + private def getNamesForEsPost12 = { + on(request) + .call("names") + .get[Array[String]] + .asSafeList + .flatMap(TemplateNamePattern.fromString) + } + + private def updateNamesForEsPre13(names: NonEmptyList[TemplateNamePattern]) = { + names.tail match { + case Nil => + case _ => + logger.warn( + s"""[${id.show}] Filtered result contains more than one template pattern. First was taken. + | The whole set of patterns [${names.toList.mkString(",")}]""".oneLiner) + } + on(request).call("name", names.head.value.value) + } + + private def updateNamesForEsPost12(names: NonEmptyList[TemplateNamePattern]): Unit = { + on(request).set("names", names.toList.map(_.value.value).toArray) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteTemplateEsRequestContext.scala new file mode 100644 index 0000000000..ea2b37d8d3 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/DeleteTemplateEsRequestContext.scala @@ -0,0 +1,67 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.TemplateNamePattern +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.DeletingLegacyTemplates +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class DeleteTemplateEsRequestContext(actionRequest: DeleteIndexTemplateRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[DeleteIndexTemplateRequest, DeletingLegacyTemplates]( + actionRequest, esContext, clusterService, threadPool + ) { + + override protected def templateOperationFrom(request: DeleteIndexTemplateRequest): DeletingLegacyTemplates = { + TemplateNamePattern.fromString(request.name()) match { + case Some(pattern) => DeletingLegacyTemplates(NonEmptyList.one(pattern)) + case None => throw RequestSeemsToBeInvalid[DeleteIndexTemplateRequest]("No template name patterns found") + } + } + + override protected def modifyRequest(blockContext: TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case DeletingLegacyTemplates(namePatterns) => + namePatterns.tail match { + case Nil => + case _ => + logger.warn( + s"""[${id.show}] Filtered result contains more than one template pattern. First was taken. + | The whole set of patterns [${namePatterns.toList.mkString(",")}]""".oneLiner) + } + actionRequest.name(namePatterns.head.value.value) + ModificationResult.Modified + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetComponentTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetComponentTemplateEsRequestContext.scala new file mode 100644 index 0000000000..7a9c6858e5 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetComponentTemplateEsRequestContext.scala @@ -0,0 +1,164 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import eu.timepit.refined.auto._ +import monix.eval.Task +import org.elasticsearch.action.admin.indices.template.get.GetComponentTemplateAction +import org.elasticsearch.cluster.metadata +import org.elasticsearch.cluster.metadata.{ComponentTemplate => MetadataComponentTemplate} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.Template.ComponentTemplate +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.GettingComponentTemplates +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, Template, TemplateName, TemplateNamePattern} +import tech.beshu.ror.accesscontrol.matchers.UniqueIdentifierGenerator +import tech.beshu.ror.accesscontrol.show.logs._ +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.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ + +class GetComponentTemplateEsRequestContext(actionRequest: GetComponentTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + (implicit generator: UniqueIdentifierGenerator) + extends BaseTemplatesEsRequestContext[GetComponentTemplateAction.Request, GettingComponentTemplates]( + actionRequest, esContext, clusterService, threadPool + ) { + + private lazy val requestTemplateNamePatterns = NonEmptyList + .fromList { + Option(actionRequest.name()).toList + .flatMap(TemplateNamePattern.fromString) + } + .getOrElse { + NonEmptyList.one(TemplateNamePattern("*")) + } + + override protected def templateOperationFrom(request: GetComponentTemplateAction.Request): GettingComponentTemplates = { + GettingComponentTemplates(requestTemplateNamePatterns) + } + + override def modifyWhenTemplateNotFound: ModificationResult = { + val nonExistentTemplateNamePattern = TemplateNamePattern.generateNonExistentBasedOn(requestTemplateNamePatterns.head) + actionRequest.name(nonExistentTemplateNamePattern.value.value) + ModificationResult.Modified + } + + override protected def modifyRequest(blockContext: BlockContext.TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case GettingComponentTemplates(namePatterns) => + if(namePatterns.tail.nonEmpty) { + logger.warn( + s"""[${id.show}] Filtered result contains more than one template. First was taken. The whole set of + | component templates [${namePatterns.show}]""".stripMargin) + } + actionRequest.name(namePatterns.head.value.value) + updateResponse(using = blockContext) + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } + + private def updateResponse(using: TemplateRequestBlockContext) = { + ModificationResult.UpdateResponse { + case r: GetComponentTemplateAction.Response => + Task.now(new GetComponentTemplateAction.Response( + filter( + templates = r.getComponentTemplates.asSafeMap, + using = using.responseTemplateTransformation + ) + )) + case other => + Task.now(other) + } + } + + private def filter(templates: Map[String, MetadataComponentTemplate], + using: Set[Template] => Set[Template]) = { + val templatesMap = templates + .flatMap { case (name, componentTemplate) => + toComponentTemplate(name, componentTemplate) match { + case Right(template) => + Some((template, (name, componentTemplate))) + case Left(msg) => + logger.error( + s"""[${id.show}] Component Template response filtering issue: $msg. For security reasons template + | [$name] will be skipped.""".oneLiner) + None + } + } + val filteredTemplates = using(templatesMap.keys.toSet) + templatesMap + .flatMap { case (template, (name, componentTemplate)) => + filteredTemplates + .find(_.name.value.value == name) + .flatMap { + case t: ComponentTemplate if t == template => + Some((name, componentTemplate)) + case t: ComponentTemplate => + Some((name, filterMetadataData(componentTemplate, t))) + case t => + logger.error(s"""[${id.show}] Expected ComponentTemplate, but got: $t. Skipping""") + None + } + } + .asJava + } + + private def filterMetadataData(componentTemplate: MetadataComponentTemplate, basedOn: ComponentTemplate) = { + new MetadataComponentTemplate( + new metadata.Template( + componentTemplate.template.settings, + componentTemplate.template.mappings, + filterAliases(componentTemplate.template, basedOn) + ), + componentTemplate.version(), + componentTemplate.metadata() + ) + } + + private def filterAliases(template: metadata.Template, basedOn: ComponentTemplate) = { + val aliasesStrings = basedOn.aliases.map(_.stringify) + template + .aliases().asSafeMap + .filter { case (name, _) => aliasesStrings.contains(name) } + .asJava + } + + private def toComponentTemplate(name: String, componentTemplate: MetadataComponentTemplate) = { + for { + name <- TemplateName + .fromString(name) + .toRight("Template name should be non-empty") + aliases = Option(componentTemplate.template()) + .map(_.aliases().asSafeMap.keys.flatMap(ClusterIndexName.fromString).toSet) + .getOrElse(Set.empty) + } yield ComponentTemplate(name, aliases) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetComposableIndexTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetComposableIndexTemplateEsRequestContext.scala new file mode 100644 index 0000000000..586cd893eb --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetComposableIndexTemplateEsRequestContext.scala @@ -0,0 +1,185 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import eu.timepit.refined.auto._ +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.admin.indices.template.get.GetComposableIndexTemplateAction +import org.elasticsearch.cluster.metadata +import org.elasticsearch.cluster.metadata.ComposableIndexTemplate +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.Template.IndexTemplate +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.GettingIndexTemplates +import tech.beshu.ror.accesscontrol.domain._ +import tech.beshu.ror.accesscontrol.matchers.UniqueIdentifierGenerator +import tech.beshu.ror.accesscontrol.request.RequestContext +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.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.jdk.CollectionConverters._ + +class GetComposableIndexTemplateEsRequestContext(actionRequest: GetComposableIndexTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + (implicit generator: UniqueIdentifierGenerator) + extends BaseTemplatesEsRequestContext[GetComposableIndexTemplateAction.Request, GettingIndexTemplates]( + actionRequest, esContext, clusterService, threadPool + ) { + + private lazy val requestTemplateNamePatterns = NonEmptyList + .fromList { + Option(actionRequest.name()).toList + .flatMap(TemplateNamePattern.fromString) + } + .getOrElse { + NonEmptyList.one(TemplateNamePattern("*")) + } + + override protected def templateOperationFrom(request: GetComposableIndexTemplateAction.Request): GettingIndexTemplates = { + GettingIndexTemplates(requestTemplateNamePatterns) + } + + override def modifyWhenTemplateNotFound: ModificationResult = { + val nonExistentTemplateNamePattern = TemplateNamePattern.generateNonExistentBasedOn(requestTemplateNamePatterns.head) + updateRequest(nonExistentTemplateNamePattern) + ModificationResult.Modified + } + + override protected def modifyRequest(blockContext: TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case GettingIndexTemplates(namePatterns) => + val templateNamePatternToUse = + if (namePatterns.tail.isEmpty) namePatterns.head + else TemplateNamePattern.findMostGenericTemplateNamePatten(namePatterns) + updateRequest(templateNamePatternToUse) + updateResponse(using = blockContext) + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } + + private def updateRequest(templateNamePattern: TemplateNamePattern): Unit = { + import org.joor.Reflect._ + on(actionRequest).set("name", templateNamePattern.value.value) + } + + private def updateResponse(using: TemplateRequestBlockContext) = { + ModificationResult.UpdateResponse { + case r: GetComposableIndexTemplateAction.Response => + Task.now(new GetComposableIndexTemplateAction.Response( + GetComposableIndexTemplateEsRequestContext + .filter( + templates = r.indexTemplates().asSafeMap, + using = using.responseTemplateTransformation + ) + .asJava + )) + case other => + Task.now(other) + } + } + +} + +private[templates] object GetComposableIndexTemplateEsRequestContext extends Logging { + + def filter(templates: Map[String, ComposableIndexTemplate], + using: Set[Template] => Set[Template]) + (implicit requestContextId: RequestContext.Id): Map[String, ComposableIndexTemplate] = { + val templatesMap = templates + .flatMap { case (name, composableIndexTemplate) => + toIndexTemplate(name, composableIndexTemplate) match { + case Right(template) => + Some((template, (name, composableIndexTemplate))) + case Left(msg) => + logger.error( + s"""[${requestContextId.show}] Template response filtering issue: $msg. For security reasons template + | [$name] will be skipped.""".oneLiner) + None + } + } + val filteredTemplates = using(templatesMap.keys.toSet) + templatesMap + .flatMap { case (template, (name, composableIndexTemplate)) => + filteredTemplates + .find(_.name == template.name) + .flatMap { + case t: IndexTemplate if t == template => + Some((name, composableIndexTemplate)) + case t: IndexTemplate => + Some((name, filterMetadataData(composableIndexTemplate, t))) + case t => + logger.error(s"""[${requestContextId.show}] Expected IndexTemplate, but got: $t. Skipping""") + None + } + } + } + + private def filterMetadataData(composableIndexTemplate: ComposableIndexTemplate, basedOn: IndexTemplate) = { + ComposableIndexTemplate + .builder() + .indexPatterns(basedOn.patterns.toList.map(_.value.stringify).asJava) + .template(new metadata.Template( + composableIndexTemplate.template().settings(), + composableIndexTemplate.template().mappings(), + filterAliases(composableIndexTemplate.template(), basedOn) + )) + .componentTemplates(composableIndexTemplate.composedOf()) + .priority(composableIndexTemplate.priority()) + .version(composableIndexTemplate.version()) + .metadata(composableIndexTemplate.metadata()) + .dataStreamTemplate(composableIndexTemplate.getDataStreamTemplate) + .allowAutoCreate(composableIndexTemplate.getAllowAutoCreate) + .ignoreMissingComponentTemplates(composableIndexTemplate.getIgnoreMissingComponentTemplates) + .deprecated(composableIndexTemplate.isDeprecated) + .build() + } + + private def filterAliases(template: metadata.Template, basedOn: IndexTemplate) = { + val aliasesStrings = basedOn.aliases.map(_.stringify) + template + .aliases().asSafeMap + .filter { case (name, _) => aliasesStrings.contains(name) } + .asJava + } + + private def toIndexTemplate(name: String, composableIndexTemplate: ComposableIndexTemplate) = { + for { + name <- TemplateName + .fromString(name) + .toRight("Template name should be non-empty") + patterns <- UniqueNonEmptyList + .fromIterable(composableIndexTemplate.indexPatterns().asSafeList.flatMap(IndexPattern.fromString)) + .toRight("Template indices pattern list should not be empty") + aliases = Option(composableIndexTemplate.template()) + .map(_.aliases().asSafeMap.keys.flatMap(ClusterIndexName.fromString).toSet) + .getOrElse(Set.empty) + } yield IndexTemplate(name, patterns, aliases) + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetTemplatesEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetTemplatesEsRequestContext.scala new file mode 100644 index 0000000000..6693dae330 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/GetTemplatesEsRequestContext.scala @@ -0,0 +1,179 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import eu.timepit.refined.auto._ +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.admin.indices.template.get.{GetIndexTemplatesRequest, GetIndexTemplatesResponse} +import org.elasticsearch.cluster.metadata.{AliasMetadata, IndexTemplateMetadata} +import org.elasticsearch.common.collect.ImmutableOpenMap +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.Template.LegacyTemplate +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.GettingLegacyTemplates +import tech.beshu.ror.accesscontrol.domain._ +import tech.beshu.ror.accesscontrol.matchers.UniqueIdentifierGenerator +import tech.beshu.ror.accesscontrol.request.RequestContext +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.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import scala.jdk.CollectionConverters._ + +class GetTemplatesEsRequestContext(actionRequest: GetIndexTemplatesRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + (implicit generator: UniqueIdentifierGenerator) + extends BaseTemplatesEsRequestContext[GetIndexTemplatesRequest, GettingLegacyTemplates]( + actionRequest, esContext, clusterService, threadPool + ) { + + private lazy val requestTemplateNamePatterns = NonEmptyList + .fromList( + actionRequest + .names().asSafeSet + .flatMap(TemplateNamePattern.fromString) + .toList + ) + .getOrElse(NonEmptyList.one(TemplateNamePattern("*"))) + + override protected def templateOperationFrom(request: GetIndexTemplatesRequest): GettingLegacyTemplates = + GettingLegacyTemplates(requestTemplateNamePatterns) + + override def modifyWhenTemplateNotFound: ModificationResult = { + val nonExistentTemplateNamePattern = TemplateNamePattern.generateNonExistentBasedOn(requestTemplateNamePatterns.head) + updateRequest(NonEmptyList.one(nonExistentTemplateNamePattern)) + ModificationResult.UpdateResponse(a => Task.delay(a)) + } + + override protected def modifyRequest(blockContext: TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case GettingLegacyTemplates(namePatterns) => + updateRequest(namePatterns) + updateResponse(using = blockContext) + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } + + private def updateRequest(templateNamePatterns: NonEmptyList[TemplateNamePattern]): Unit = { + import org.joor.Reflect._ + on(actionRequest).set("names", templateNamePatterns.map(_.value.value).toList.toArray) + } + + private def updateResponse(using: TemplateRequestBlockContext) = { + ModificationResult.UpdateResponse { + case r: GetIndexTemplatesResponse => + Task.now(new GetIndexTemplatesResponse( + GetTemplatesEsRequestContext + .filter( + templates = r.getIndexTemplates.asSafeList, + using = using.responseTemplateTransformation + ) + .asJava + )) + case other => + Task.now(other) + } + } + +} + +private[templates] object GetTemplatesEsRequestContext extends Logging { + + def filter(templates: List[IndexTemplateMetadata], + using: Set[Template] => Set[Template]) + (implicit requestContextId: RequestContext.Id): List[IndexTemplateMetadata] = { + val templatesMap = templates + .flatMap { metadata => + toLegacyTemplate(metadata) match { + case Right(template) => + Some((template, metadata)) + case Left(msg) => + logger.error( + s"""[${requestContextId.show}] Template response filtering issue: $msg. For security reasons template + | [${metadata.name()}] will be skipped.""".oneLiner) + None + } + } + .toMap + val filteredTemplates = using(templatesMap.keys.toSet) + templatesMap + .flatMap { case (template, metadata) => + filteredTemplates + .find(_.name == template.name) + .flatMap { + case t: LegacyTemplate if t == template => + Some(metadata) + case t: LegacyTemplate => + Some(filterMetadataData(metadata, t)) + case t => + logger.error(s"""[${requestContextId.show}] Expected IndexTemplate, but got: $t. Skipping""") + None + } + } + .toList + } + + private def filterMetadataData(metadata: IndexTemplateMetadata, basedOn: LegacyTemplate) = { + new IndexTemplateMetadata( + metadata.name(), + metadata.order(), + metadata.version(), + basedOn.patterns.toList.map(_.value.stringify).asJava, + metadata.settings(), + Map(metadata.mappings().string() -> metadata.mappings()).asJava, + filterAliases(metadata, basedOn) + ) + } + + private def filterAliases(metadata: IndexTemplateMetadata, template: LegacyTemplate) = { + val aliasesStrings = template.aliases.map(_.stringify) + val filteredAliasesMap = + metadata + .aliases().asSafeValues + .filter { a => aliasesStrings.contains(a.alias()) } + .map(a => (a.alias(), a)) + .toMap + ImmutableOpenMap + .builder[String, AliasMetadata]() + .putAllFromMap(filteredAliasesMap.asJava) + .build() + } + + private def toLegacyTemplate(metadata: IndexTemplateMetadata) = { + for { + name <- TemplateName + .fromString(metadata.getName) + .toRight("Template name should be non-empty") + patterns <- UniqueNonEmptyList + .fromIterable(metadata.patterns().asSafeList.flatMap(IndexPattern.fromString)) + .toRight("Template indices pattern list should not be empty") + aliases = metadata.aliases().asSafeKeys.flatMap(ClusterIndexName.fromString) + } yield LegacyTemplate(name, patterns, aliases) + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/MultiSearchTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/MultiSearchTemplateEsRequestContext.scala new file mode 100644 index 0000000000..b592cd70a4 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/MultiSearchTemplateEsRequestContext.scala @@ -0,0 +1,179 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import org.elasticsearch.action.search.MultiSearchResponse +import org.elasticsearch.action.{ActionRequest, ActionResponse, CompositeIndicesRequest} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.FilterableMultiRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.MultiIndexRequestBlockContext.Indices +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage.NotUsingFields +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.Strategy.BasedOnBlockContextOnly +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, FieldLevelSecurity, Filter} +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.accesscontrol.utils.IndicesListOps._ +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.SearchRequestOps._ +import tech.beshu.ror.es.handler.request.context.ModificationResult.{Modified, ShouldBeInterrupted} +import tech.beshu.ror.es.handler.request.context.types.ReflectionBasedActionRequest +import tech.beshu.ror.es.handler.request.context.{BaseEsRequestContext, EsRequest, ModificationResult} +import tech.beshu.ror.es.handler.response.SearchHitOps._ +import tech.beshu.ror.utils.ScalaOps._ + +class MultiSearchTemplateEsRequestContext private(actionRequest: ActionRequest with CompositeIndicesRequest, + esContext: EsContext, + clusterService: RorClusterService, + override implicit val threadPool: ThreadPool) + extends BaseEsRequestContext[FilterableMultiRequestBlockContext](esContext, clusterService) + with EsRequest[FilterableMultiRequestBlockContext] { + + override lazy val initialBlockContext: FilterableMultiRequestBlockContext = FilterableMultiRequestBlockContext( + this, + UserMetadata.from(this), + Set.empty, + List.empty, + indexPacksFrom(multiSearchTemplateRequest), + None, + None, + requestFieldsUsage + ) + + private lazy val multiSearchTemplateRequest = new ReflectionBasedMultiSearchTemplateRequest(actionRequest) + + override protected def modifyRequest(blockContext: FilterableMultiRequestBlockContext): ModificationResult = { + val modifiedPacksOfIndices = blockContext.indexPacks + val requests = multiSearchTemplateRequest.requests + if (requests.size == modifiedPacksOfIndices.size) { + requests + .zip(modifiedPacksOfIndices) + .foreach { case (request, pack) => + updateRequest(request, pack, blockContext.filter, blockContext.fieldLevelSecurity) + } + ModificationResult.UpdateResponse.using(filterFieldsFromResponse(blockContext.fieldLevelSecurity)) + } else { + logger.error(s"[${id.show}] Cannot alter MultiSearchRequest request, because origin request contained different number of" + + s" inner requests, than altered one. This can be security issue. So, it's better for forbid the request") + ShouldBeInterrupted + } + } + + private def requestFieldsUsage: RequestFieldsUsage = { + NonEmptyList.fromList(multiSearchTemplateRequest.requests) match { + case Some(definedRequests) => + definedRequests + .map(_.getRequest.checkFieldsUsage()) + .combineAll + case None => + NotUsingFields + } + } + + private def filterFieldsFromResponse(fieldLevelSecurity: Option[FieldLevelSecurity]) + (actionResponse: ActionResponse): ActionResponse = { + (actionResponse, fieldLevelSecurity) match { + case (response: MultiSearchResponse, Some(FieldLevelSecurity(restrictions, _: BasedOnBlockContextOnly))) => + response.getResponses + .filterNot(_.isFailure) + .flatMap(_.getResponse.getHits.getHits) + .foreach { hit => + hit + .filterSourceFieldsUsing(restrictions) + .filterDocumentFieldsUsing(restrictions) + } + response + case _ => + actionResponse + } + } + + override def modifyWhenIndexNotFound: ModificationResult = { + multiSearchTemplateRequest.requests.foreach(updateRequestWithNonExistingIndex) + Modified + } + + private def indexPacksFrom(request: ReflectionBasedMultiSearchTemplateRequest): List[Indices] = { + request + .requests + .map { request => Indices.Found(indicesFrom(request)) } + } + + private def updateRequest(request: ReflectionBasedSearchTemplateRequest, + indexPack: Indices, + filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]): Unit = { + val nonEmptyIndicesList = indexPack match { + case Indices.Found(indices) => + NonEmptyList + .fromList(indices.toList) + .getOrElse(NonEmptyList.one(randomNonexistentIndex(request))) + case Indices.Found(_) | Indices.NotFound => + NonEmptyList.one(randomNonexistentIndex(request)) + } + request.setRequest( + request.getRequest, nonEmptyIndicesList, filter, fieldLevelSecurity + ) + } + + private def updateRequestWithNonExistingIndex(request: ReflectionBasedSearchTemplateRequest): Unit = { + request.setRequest( + request.getRequest, NonEmptyList.one(randomNonexistentIndex(request)), None, None + ) + } + + private def randomNonexistentIndex(request: ReflectionBasedSearchTemplateRequest) = + indicesFrom(request).toList.randomNonexistentIndex() + + private def indicesFrom(request: ReflectionBasedSearchTemplateRequest) = { + val requestIndices = request.getRequest.indices.asSafeSet.flatMap(ClusterIndexName.fromString) + indicesOrWildcard(requestIndices) + } +} + +object MultiSearchTemplateEsRequestContext { + def unapply(arg: ReflectionBasedActionRequest): Option[MultiSearchTemplateEsRequestContext] = { + if (arg.esContext.actionRequest.getClass.getSimpleName.startsWith("MultiSearchTemplateRequest")) { + Some(new MultiSearchTemplateEsRequestContext( + arg.esContext.actionRequest.asInstanceOf[ActionRequest with CompositeIndicesRequest], + arg.esContext, + arg.clusterService, + arg.threadPool + )) + } else { + None + } + } +} + +private class ReflectionBasedMultiSearchTemplateRequest(val actionRequest: ActionRequest) + (implicit val requestContext: RequestContext.Id, + threadPool: ThreadPool) { + + import org.joor.Reflect.on + + def requests: List[ReflectionBasedSearchTemplateRequest] = { + on(actionRequest) + .call("requests") + .get[java.util.List[ActionRequest]] + .asSafeList + .map(new ReflectionBasedSearchTemplateRequest(_)) + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutComponentTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutComponentTemplateEsRequestContext.scala new file mode 100644 index 0000000000..48403a3620 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutComponentTemplateEsRequestContext.scala @@ -0,0 +1,58 @@ +/* + * 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.templates + +import org.elasticsearch.action.admin.indices.template.put.PutComponentTemplateAction +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.AddingComponentTemplate +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, TemplateName} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.types.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +class PutComponentTemplateEsRequestContext(actionRequest: PutComponentTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[PutComponentTemplateAction.Request, AddingComponentTemplate]( + actionRequest, esContext, clusterService, threadPool + ) { + + override protected def templateOperationFrom(request: PutComponentTemplateAction.Request): AddingComponentTemplate = { + val templateOperation = for { + name <- TemplateName + .fromString(request.name()) + .toRight("Template name should be non-empty") + aliases = request.componentTemplate().template().aliases().asSafeMap.keys.flatMap(ClusterIndexName.fromString).toSet + } yield AddingComponentTemplate(name, aliases) + + templateOperation match { + case Right(operation) => operation + case Left(msg) => throw RequestSeemsToBeInvalid[PutComponentTemplateAction.Request](msg) + } + } + + override protected def modifyRequest(blockContext: BlockContext.TemplateRequestBlockContext): ModificationResult = { + // nothing to modify - if it wasn't blocked, we are good + Modified + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutComposableIndexTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutComposableIndexTemplateEsRequestContext.scala new file mode 100644 index 0000000000..8d285362d9 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutComposableIndexTemplateEsRequestContext.scala @@ -0,0 +1,68 @@ +/* + * 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.templates + +import org.elasticsearch.action.admin.indices.template.put.TransportPutComposableIndexTemplateAction +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.AddingIndexTemplate +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, IndexPattern, TemplateName} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.types.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +class PutComposableIndexTemplateEsRequestContext(actionRequest: TransportPutComposableIndexTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[TransportPutComposableIndexTemplateAction.Request, AddingIndexTemplate]( + actionRequest, esContext, clusterService, threadPool + ) { + + override protected def templateOperationFrom(request: TransportPutComposableIndexTemplateAction.Request): AddingIndexTemplate = { + PutComposableIndexTemplateEsRequestContext.templateOperationFrom(request) match { + case Right(operation) => operation + case Left(msg) => throw RequestSeemsToBeInvalid[TransportPutComposableIndexTemplateAction.Request](msg) + } + } + + override protected def modifyRequest(blockContext: BlockContext.TemplateRequestBlockContext): ModificationResult = { + // nothing to modify - if it wasn't blocked, we are good + Modified + } +} + +object PutComposableIndexTemplateEsRequestContext { + + private [types] def templateOperationFrom(request: TransportPutComposableIndexTemplateAction.Request): Either[String, AddingIndexTemplate] = { + for { + name <- TemplateName + .fromString(request.name()) + .toRight("Template name should be non-empty") + patterns <- UniqueNonEmptyList + .fromIterable(request.indexTemplate().indexPatterns().asSafeList.flatMap(IndexPattern.fromString)) + .toRight("Template indices pattern list should not be empty") + aliases = request.indexTemplate().template().asSafeSet + .flatMap(_.aliases().asSafeMap.keys.flatMap(ClusterIndexName.fromString).toSet) + } yield AddingIndexTemplate(name, patterns, aliases) + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutTemplateEsRequestContext.scala new file mode 100644 index 0000000000..0365577dd0 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/PutTemplateEsRequestContext.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.templates + +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.AddingLegacyTemplate +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, IndexPattern, TemplateName} +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.ModificationResult.Modified +import tech.beshu.ror.es.handler.request.context.types.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +class PutTemplateEsRequestContext(actionRequest: PutIndexTemplateRequest, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[PutIndexTemplateRequest, AddingLegacyTemplate]( + actionRequest, esContext, clusterService, threadPool + ) { + + override protected def templateOperationFrom(request: PutIndexTemplateRequest): AddingLegacyTemplate = { + val templateOperation = for { + name <- TemplateName + .fromString(request.name()) + .toRight("Template name should be non-empty") + patterns <- UniqueNonEmptyList + .fromIterable(request.patterns().asSafeList.flatMap(IndexPattern.fromString)) + .toRight("Template indices pattern list should not be empty") + aliases = request.aliases().asSafeSet.flatMap(a => ClusterIndexName.fromString(a.name())) + } yield AddingLegacyTemplate(name, patterns, aliases) + + templateOperation match { + case Right(operation) => operation + case Left(msg) => throw RequestSeemsToBeInvalid[PutIndexTemplateRequest](msg) + } + } + + override protected def modifyRequest(blockContext: TemplateRequestBlockContext): ModificationResult = { + // nothing to modify - if it wasn't blocked, we are good + Modified + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SearchTemplateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SearchTemplateEsRequestContext.scala new file mode 100644 index 0000000000..247379d2c7 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SearchTemplateEsRequestContext.scala @@ -0,0 +1,161 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import org.elasticsearch.action.search.{SearchRequest, SearchResponse} +import org.elasticsearch.action.{ActionRequest, ActionResponse, CompositeIndicesRequest} +import org.elasticsearch.search.builder.SearchSourceBuilder +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.Strategy.BasedOnBlockContextOnly +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, FieldLevelSecurity, Filter} +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.request.SearchRequestOps._ +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.{BaseFilterableEsRequestContext, ReflectionBasedActionRequest} +import tech.beshu.ror.es.handler.response.SearchHitOps._ +import tech.beshu.ror.utils.ScalaOps._ + +class SearchTemplateEsRequestContext private(actionRequest: ActionRequest with CompositeIndicesRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override implicit val threadPool: ThreadPool) + extends BaseFilterableEsRequestContext[ActionRequest with CompositeIndicesRequest]( + actionRequest, esContext, aclContext, clusterService, threadPool + ) { + + private lazy val searchTemplateRequest = new ReflectionBasedSearchTemplateRequest(actionRequest) + private lazy val searchRequest = searchTemplateRequest.getRequest + + override protected def requestFieldsUsage: FieldLevelSecurity.RequestFieldsUsage = + searchTemplateRequest.getRequest.checkFieldsUsage() + + override protected def indicesFrom(request: ActionRequest with CompositeIndicesRequest): Set[ClusterIndexName] = { + searchRequest + .indices.asSafeSet + .flatMap(ClusterIndexName.fromString) + } + + override protected def update(request: ActionRequest with CompositeIndicesRequest, + indices: NonEmptyList[ClusterIndexName], + filter: Option[domain.Filter], + fieldLevelSecurity: Option[domain.FieldLevelSecurity]): ModificationResult = { + searchTemplateRequest.setRequest( + searchRequest, indices, filter, fieldLevelSecurity + ) + ModificationResult.UpdateResponse.using(filterFieldsFromResponse(fieldLevelSecurity)) + } + + private def filterFieldsFromResponse(fieldLevelSecurity: Option[FieldLevelSecurity]) + (actionResponse: ActionResponse): ActionResponse = { + val searchTemplateResponse = new ReflectionBasedSearchTemplateResponse(actionResponse) + (searchTemplateResponse.getResponse, fieldLevelSecurity) match { + case (Some(response), Some(FieldLevelSecurity(restrictions, _: BasedOnBlockContextOnly))) => + response.getHits.getHits + .foreach { hit => + hit + .filterSourceFieldsUsing(restrictions) + .filterDocumentFieldsUsing(restrictions) + } + actionResponse + case _ => + actionResponse + } + } +} + +object SearchTemplateEsRequestContext { + def unapply(arg: ReflectionBasedActionRequest): Option[SearchTemplateEsRequestContext] = { + if (arg.esContext.actionRequest.getClass.getSimpleName.startsWith("SearchTemplateRequest")) { + Some(new SearchTemplateEsRequestContext( + arg.esContext.actionRequest.asInstanceOf[ActionRequest with CompositeIndicesRequest], + arg.esContext, + arg.aclContext, + arg.clusterService, + arg.threadPool + )) + } else { + None + } + } +} + +final class ReflectionBasedSearchTemplateRequest(actionRequest: ActionRequest) + (implicit threadPool: ThreadPool, + requestId: RequestContext.Id) { + + import org.joor.Reflect.on + + def getRequest: SearchRequest = { + Option(on(actionRequest) + .call("getRequest") + .get[SearchRequest]) match { + case Some(sr) => sr + case None => + val sr = new SearchRequest("*") + setSearchRequest(sr) + sr + } + } + + def setRequest(searchRequest: SearchRequest, + indices: NonEmptyList[ClusterIndexName], + filter: Option[domain.Filter], + fieldLevelSecurity: Option[domain.FieldLevelSecurity]): Unit = { + setSearchRequest(new EnhancedSearchRequest(searchRequest, indices, filter, fieldLevelSecurity)) + } + + private def setSearchRequest(searchRequest: SearchRequest) = { + on(actionRequest).call("setRequest", searchRequest) + } + + private class EnhancedSearchRequest(request: SearchRequest, + indices: NonEmptyList[ClusterIndexName], + filter: Option[Filter], + fieldLevelSecurity: Option[FieldLevelSecurity]) + (implicit threadPool: ThreadPool, + requestId: RequestContext.Id) + extends SearchRequest(request) { + + this.indices(indices.toList.map(_.stringify): _*) + + override def source(sourceBuilder: SearchSourceBuilder): SearchRequest = { + super + .source(sourceBuilder) + .applyFilterToQuery(filter) + .applyFieldLevelSecurity(fieldLevelSecurity) + } + } +} + +final class ReflectionBasedSearchTemplateResponse(actionResponse: ActionResponse) { + + import org.joor.Reflect.on + + def getResponse: Option[SearchResponse] = { + Option( + on(actionResponse) + .call("getResponse") + .get[SearchResponse] + ) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SimulateIndexTemplateRequestEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SimulateIndexTemplateRequestEsRequestContext.scala new file mode 100644 index 0000000000..27dee5ec61 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SimulateIndexTemplateRequestEsRequestContext.scala @@ -0,0 +1,164 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import eu.timepit.refined.types.string.NonEmptyString +import monix.eval.Task +import org.elasticsearch.action.admin.indices.template.post.{SimulateIndexTemplateRequest, SimulateIndexTemplateResponse} +import org.elasticsearch.cluster.metadata.{Template => EsMetadataTemplate} +import org.elasticsearch.threadpool.ThreadPool +import org.joor.Reflect.on +import tech.beshu.ror.accesscontrol.AccessControl.AccessControlStaticContext +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, IndexPattern, TemplateNamePattern} +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.utils.ScalaOps._ + +import java.util.{List => JList, Map => JMap} +import scala.jdk.CollectionConverters._ + +class SimulateIndexTemplateRequestEsRequestContext(actionRequest: SimulateIndexTemplateRequest, + esContext: EsContext, + aclContext: AccessControlStaticContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + // note: it may seem that it's template request but it's not. It's rather related with index and that's why we treat it in this way + extends BaseIndicesEsRequestContext(actionRequest, esContext, aclContext, clusterService, threadPool) { + + override lazy val isReadOnlyRequest: Boolean = true + + override protected def indicesFrom(request: SimulateIndexTemplateRequest): Set[ClusterIndexName] = + Option(request.getIndexName) + .flatMap(ClusterIndexName.fromString) + .toSet + + override protected def update(request: SimulateIndexTemplateRequest, + filteredIndices: NonEmptyList[ClusterIndexName], + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + if (filteredIndices.tail.nonEmpty) { + logger.warn(s"[${id.show}] Filtered result contains more than one index. First was taken. The whole set of indices [${filteredIndices.toList.mkString(",")}]") + } + update(request, filteredIndices.head, allAllowedIndices) + } + + private def update(request: SimulateIndexTemplateRequest, + index: ClusterIndexName, + allAllowedIndices: NonEmptyList[ClusterIndexName]): ModificationResult = { + request.indexName(index.stringify) + ModificationResult.UpdateResponse { + case response: SimulateIndexTemplateResponse => + Task.now(SimulateIndexTemplateRequestEsRequestContext.filterAliasesAndIndexPatternsIn(response, allAllowedIndices.toList)) + case other => + Task.now(other) + } + } +} + +object SimulateIndexTemplateRequestEsRequestContext { + + private[templates] def filterAliasesAndIndexPatternsIn(response: SimulateIndexTemplateResponse, + allowedIndices: List[ClusterIndexName]): SimulateIndexTemplateResponse = { + val tunedResponse = new TunedSimulateIndexTemplateResponse(response) + val filterResponse = filterIndexTemplate(allowedIndices) andThen filterOverlappingTemplates(allowedIndices) + filterResponse(tunedResponse).underlying + } + + private def filterIndexTemplate(allowedIndices: List[ClusterIndexName]) = (response: TunedSimulateIndexTemplateResponse) => { + response + .indexTemplateRequest() + .map { template => + val newTemplate = createMetadataTemplateWithFilteredAliases( + basedOn = template, + allowedIndices + ) + response.indexTemplateRequest(newTemplate) + } + .getOrElse { + response + } + } + + private def filterOverlappingTemplates(allowedIndices: List[ClusterIndexName]) = (response: TunedSimulateIndexTemplateResponse) => { + val filteredOverlappingTemplates = createOverlappingTemplatesWithFilteredIndexPatterns( + basedOn = response.overlappingTemplates(), + allowedIndices + ) + response.overlappingTemplates(filteredOverlappingTemplates) + } + + private def createMetadataTemplateWithFilteredAliases(basedOn: EsMetadataTemplate, + allowedIndices: List[ClusterIndexName]) = { + val filteredAliases = basedOn + .aliases().asSafeMap + .flatMap { case (key, value) => ClusterIndexName.fromString(key).map((_, value)) } + .view + .filterKeys(_.isAllowedBy(allowedIndices.toSet)) + .map { case (key, value) => (key.stringify, value) } + .toMap + .asJava + new EsMetadataTemplate( + basedOn.settings(), + basedOn.mappings(), + filteredAliases + ) + } + + private def createOverlappingTemplatesWithFilteredIndexPatterns(basedOn: Map[TemplateNamePattern, List[IndexPattern]], + allowedIndices: List[ClusterIndexName]) = { + basedOn.flatMap { case (templateName, patterns) => + val filteredPatterns = patterns.filter(_.isAllowedByAny(allowedIndices)) + filteredPatterns match { + case Nil => None + case _ => Some((templateName, filteredPatterns)) + } + } + } + + private[templates] class TunedSimulateIndexTemplateResponse(val underlying: SimulateIndexTemplateResponse) { + + private val reflect = on(underlying) + private val resolvedTemplateFieldName = "resolvedTemplate" + private val overlappingTemplatesFieldName = "overlappingTemplates" + + def indexTemplateRequest(): Option[EsMetadataTemplate] = + Option(reflect.get[EsMetadataTemplate](resolvedTemplateFieldName)) + + def indexTemplateRequest(template: EsMetadataTemplate): TunedSimulateIndexTemplateResponse = { + reflect.set(resolvedTemplateFieldName, template) + this + } + + def overlappingTemplates(): Map[TemplateNamePattern, List[IndexPattern]] = { + Option(reflect.get[JMap[String, JList[String]]](overlappingTemplatesFieldName)) + .map(_.asSafeMap) + .getOrElse(Map.empty) + .map { case (key, value) => + (TemplateNamePattern(NonEmptyString.unsafeFrom(key)), value.asSafeList.flatMap(IndexPattern.fromString)) + } + } + + def overlappingTemplates(templates: Map[TemplateNamePattern, List[IndexPattern]]): TunedSimulateIndexTemplateResponse = { + val jTemplatesMap = templates.map { case (key, value) => (key.value.value, value.map(_.value.stringify).asJava) }.asJava + reflect.set(overlappingTemplatesFieldName, jTemplatesMap) + this + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SimulateTemplateRequestEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SimulateTemplateRequestEsRequestContext.scala new file mode 100644 index 0000000000..27035629dc --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/SimulateTemplateRequestEsRequestContext.scala @@ -0,0 +1,160 @@ +/* + * 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.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import monix.eval.Task +import org.elasticsearch.action.admin.indices.template.post.{SimulateIndexTemplateResponse, SimulateTemplateAction} +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.{AddingIndexTemplateAndGetAllowedOnes, GettingIndexTemplates} +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, TemplateNamePattern, TemplateOperation} +import tech.beshu.ror.accesscontrol.matchers.UniqueIdentifierGenerator +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.es.handler.request.context.ModificationResult +import tech.beshu.ror.es.handler.request.context.types.BaseTemplatesEsRequestContext +import tech.beshu.ror.es.handler.request.context.types.templates.SimulateIndexTemplateRequestEsRequestContext.TunedSimulateIndexTemplateResponse +import tech.beshu.ror.utils.ScalaOps._ + +object SimulateTemplateRequestEsRequestContext { + def from(actionRequest: SimulateTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + threadPool: ThreadPool) + (implicit generator: UniqueIdentifierGenerator): SimulateTemplateRequestEsRequestContext[_ <: TemplateOperation] = { + Option(actionRequest.getTemplateName).flatMap(TemplateNamePattern.fromString) match { + case Some(templateName) => + new SimulateExistingTemplateRequestEsRequestContext(templateName, actionRequest, esContext, clusterService, threadPool) + case None => + new SimulateNewTemplateRequestEsRequestContext(actionRequest, esContext, clusterService, threadPool) + } + } +} + +class SimulateNewTemplateRequestEsRequestContext(actionRequest: SimulateTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends SimulateTemplateRequestEsRequestContext[AddingIndexTemplateAndGetAllowedOnes]( + actionRequest, esContext, clusterService, threadPool + ) { + + override protected def templateOperationFrom(actionRequest: SimulateTemplateAction.Request): AddingIndexTemplateAndGetAllowedOnes = { + Option(actionRequest.getIndexTemplateRequest) + .map { newTemplateRequest => + PutComposableIndexTemplateEsRequestContext.templateOperationFrom(newTemplateRequest) match { + case Right(operation) => AddingIndexTemplateAndGetAllowedOnes( + operation.name, operation.patterns, operation.aliases, List(TemplateNamePattern.wildcard) + ) + case Left(msg) => + throw RequestSeemsToBeInvalid[SimulateTemplateAction.Request](msg) + } + } + .getOrElse { + throw RequestSeemsToBeInvalid[SimulateTemplateAction.Request]("Index template definition doesn't exist") + } + } + + override protected def modifyRequest(blockContext: BlockContext.TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case AddingIndexTemplateAndGetAllowedOnes(_, _, _, allowedTemplates) => + updateResponse(allowedTemplates, blockContext.allAllowedIndices.toList) + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } +} + +class SimulateExistingTemplateRequestEsRequestContext(existingTemplateName: TemplateNamePattern, + actionRequest: SimulateTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + (implicit generator: UniqueIdentifierGenerator) + extends SimulateTemplateRequestEsRequestContext[GettingIndexTemplates](actionRequest, esContext, clusterService, threadPool) { + + override protected def templateOperationFrom(actionRequest: SimulateTemplateAction.Request): GettingIndexTemplates = + GettingIndexTemplates(NonEmptyList.of(existingTemplateName)) + + override def modifyWhenTemplateNotFound: ModificationResult = { + val nonExistentTemplateNamePattern = TemplateNamePattern.generateNonExistentBasedOn(existingTemplateName) + actionRequest.templateName(nonExistentTemplateNamePattern.value.value) + ModificationResult.Modified + } + + override protected def modifyRequest(blockContext: BlockContext.TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case GettingIndexTemplates(namePatterns) => + namePatterns.find(_ == existingTemplateName) match { + case Some(_) => + updateResponse(namePatterns.toList, blockContext.allAllowedIndices.toList) + case None => + logger.info(s"[${id.show}] User has no access to template ${existingTemplateName.value.value}") + ModificationResult.ShouldBeInterrupted + } + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } +} + +abstract class SimulateTemplateRequestEsRequestContext[O <: TemplateOperation](actionRequest: SimulateTemplateAction.Request, + esContext: EsContext, + clusterService: RorClusterService, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[SimulateTemplateAction.Request, O](actionRequest, esContext, clusterService, threadPool) { + + protected def updateResponse(allowedTemplates: List[TemplateNamePattern], + allowedIndices: List[ClusterIndexName]): ModificationResult.UpdateResponse = { + ModificationResult.UpdateResponse { + case response: SimulateIndexTemplateResponse => + Task.now(filterTemplatesIn(response, allowedTemplates, allowedIndices)) + case other => + Task.now(other) + } + } + + private def filterTemplatesIn(response: SimulateIndexTemplateResponse, + allowedTemplates: List[TemplateNamePattern], + allowedIndices: List[ClusterIndexName]): SimulateIndexTemplateResponse = { + val tunedResponse = new TunedSimulateIndexTemplateResponse(response) + val filterResponse = filterOverlappingTemplates(allowedTemplates) andThen filterAliasesAndIndexPatternsIn(allowedIndices) + filterResponse(tunedResponse).underlying + } + + private def filterOverlappingTemplates(templates: List[TemplateNamePattern]) = (response: TunedSimulateIndexTemplateResponse) => { + val filteredOverlappingTemplates = response + .overlappingTemplates() + .filter { case (key, _) => templates.contains(key) } + response.overlappingTemplates(filteredOverlappingTemplates) + } + + private def filterAliasesAndIndexPatternsIn(allowedIndices: List[ClusterIndexName]) = (response: TunedSimulateIndexTemplateResponse) => { + new TunedSimulateIndexTemplateResponse( + SimulateIndexTemplateRequestEsRequestContext.filterAliasesAndIndexPatternsIn(response.underlying, allowedIndices) + ) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/TemplateClusterStateEsRequestContext.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/TemplateClusterStateEsRequestContext.scala new file mode 100644 index 0000000000..3cc915681a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/templates/TemplateClusterStateEsRequestContext.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.es.handler.request.context.types.templates + +import cats.data.NonEmptyList +import cats.implicits._ +import eu.timepit.refined.auto._ +import monix.eval.Task +import org.elasticsearch.action.admin.cluster.state.{ClusterStateRequest, ClusterStateResponse} +import org.elasticsearch.cluster.metadata.Metadata +import org.elasticsearch.cluster.{ClusterName, ClusterState} +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.TemplateRequestBlockContext.TemplatesTransformation +import tech.beshu.ror.accesscontrol.domain.TemplateOperation.{GettingIndexTemplates, GettingLegacyAndIndexTemplates, GettingLegacyTemplates} +import tech.beshu.ror.accesscontrol.domain.UriPath.{CatTemplatePath, TemplatePath} +import tech.beshu.ror.accesscontrol.domain.{TemplateName, TemplateNamePattern, UriPath} +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.BaseTemplatesEsRequestContext +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ + +object TemplateClusterStateEsRequestContext { + + def from(actionRequest: ClusterStateRequest, + esContext: EsContext, + clusterService: RorClusterService, + settings: Settings, + threadPool: ThreadPool): Option[TemplateClusterStateEsRequestContext] = { + UriPath.from(esContext.channel.request().uri()) match { + case Some(TemplatePath(_) | CatTemplatePath(_)) => + Some(new TemplateClusterStateEsRequestContext(actionRequest, esContext, clusterService, settings, threadPool)) + case _ => + None + } + } +} + +class TemplateClusterStateEsRequestContext private(actionRequest: ClusterStateRequest, + esContext: EsContext, + clusterService: RorClusterService, + settings: Settings, + override val threadPool: ThreadPool) + extends BaseTemplatesEsRequestContext[ClusterStateRequest, GettingLegacyAndIndexTemplates]( + actionRequest, esContext, clusterService, threadPool + ) { + + private lazy val allTemplatesNamePattern = TemplateNamePattern("*") + + override protected def templateOperationFrom(request: ClusterStateRequest): GettingLegacyAndIndexTemplates = { + GettingLegacyAndIndexTemplates( + GettingLegacyTemplates(NonEmptyList.one(allTemplatesNamePattern)), + GettingIndexTemplates(NonEmptyList.one(allTemplatesNamePattern)) + ) + } + + override def modifyWhenTemplateNotFound: ModificationResult = { + ModificationResult.UpdateResponse(_ => Task.now(emptyClusterResponse)) + } + + override protected def modifyRequest(blockContext: TemplateRequestBlockContext): ModificationResult = { + blockContext.templateOperation match { + case GettingLegacyAndIndexTemplates(legacyTemplatesOperation, indexTemplatesOperation) => + updateResponse { + val func1 = modifyLegacyTemplatesOfResponse(_, legacyTemplatesOperation.namePatterns.toList.toSet, blockContext.responseTemplateTransformation) + val func2 = modifyIndexTemplatesOfResponse(_, indexTemplatesOperation.namePatterns.toList.toSet, blockContext.responseTemplateTransformation) + func1 andThen func2 + } + case GettingLegacyTemplates(namePatterns) => + updateResponse( + modifyLegacyTemplatesOfResponse(_, namePatterns.toList.toSet, blockContext.responseTemplateTransformation) + ) + case GettingIndexTemplates(namePatterns) => + updateResponse( + modifyIndexTemplatesOfResponse(_, namePatterns.toList.toSet, blockContext.responseTemplateTransformation) + ) + case other => + logger.error( + s"""[${id.show}] Cannot modify templates request because of invalid operation returned by ACL (operation + | type [${other.getClass}]]. Please report the issue!""".oneLiner) + ModificationResult.ShouldBeInterrupted + } + } + + private def updateResponse(func: ClusterStateResponse => ClusterStateResponse) = { + ModificationResult.UpdateResponse { + case response: ClusterStateResponse => + Task.delay(func(response)) + case other => + Task.now(other) + } + } + + private def modifyLegacyTemplatesOfResponse(response: ClusterStateResponse, + allowedTemplates: Set[TemplateNamePattern], + transformation: TemplatesTransformation) = { + val oldMetadata = response.getState.metadata() + val filteredTemplates = GetTemplatesEsRequestContext + .filter( + oldMetadata.templates().values().asScala.toList, + transformation + ) + .filter { t => + TemplateName + .fromString(t.name()) + .exists { templateName => + allowedTemplates.exists(_.matches(templateName)) + } + } + .map(_.name()) + + val newMetadataWithFilteredTemplates = oldMetadata + .templates().keySet().asScala + .foldLeft(Metadata.builder(oldMetadata)) { + case (acc, templateName) if filteredTemplates.contains(templateName) => acc + case (acc, templateName) => acc.removeTemplate(templateName) + } + .build() + + val modifiedClusterState = + ClusterState + .builder(response.getState) + .metadata(newMetadataWithFilteredTemplates) + .build() + + new ClusterStateResponse( + response.getClusterName, + modifiedClusterState, + response.isWaitForTimedOut + ) + } + + private def modifyIndexTemplatesOfResponse(response: ClusterStateResponse, + allowedTemplates: Set[TemplateNamePattern], + transformation: TemplatesTransformation) = { + val oldMetadata = response.getState.metadata() + + val filteredTemplatesV2 = + GetComposableIndexTemplateEsRequestContext + .filter( + oldMetadata.templatesV2().asSafeMap, + transformation + ) + .keys + .filter { name => + TemplateName + .fromString(name) + .exists { templateName => + allowedTemplates.exists(_.matches(templateName)) + } + } + .toSet + + val newMetadataWithFilteredTemplatesV2 = oldMetadata + .templatesV2().keySet().asScala + .foldLeft(Metadata.builder(oldMetadata)) { + case (acc, templateName) if filteredTemplatesV2.contains(templateName) => acc + case (acc, templateName) => acc.removeIndexTemplate(templateName) + } + .build() + + val modifiedClusterState = + ClusterState + .builder(response.getState) + .metadata(newMetadataWithFilteredTemplatesV2) + .build() + + new ClusterStateResponse( + response.getClusterName, + modifiedClusterState, + response.isWaitForTimedOut + ) + } + + private lazy val emptyClusterResponse = { + new ClusterStateResponse( + ClusterName.CLUSTER_NAME_SETTING.get(settings), ClusterState.EMPTY_STATE, false + ) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/utils/FilterableAliasesMap.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/utils/FilterableAliasesMap.scala new file mode 100644 index 0000000000..64827a3564 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/context/types/utils/FilterableAliasesMap.scala @@ -0,0 +1,57 @@ +/* + * 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.utils + +import cats.data.NonEmptyList +import org.elasticsearch.cluster.metadata.AliasMetadata +import tech.beshu.ror.accesscontrol.domain.ClusterIndexName +import tech.beshu.ror.accesscontrol.matchers.PatternsMatcher.{Conversion, Matchable} +import tech.beshu.ror.accesscontrol.matchers.PatternsMatcher +import tech.beshu.ror.es.handler.request.context.types.utils.FilterableAliasesMap.AliasesMap +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ +import scala.language.implicitConversions + +class FilterableAliasesMap(val value: AliasesMap) extends AnyVal { + + import FilterableAliasesMap._ + + def filterOutNotAllowedAliases(allowedAliases: NonEmptyList[ClusterIndexName]): AliasesMap = { + filter(value.asSafeMap.toList, allowedAliases).toMap.asJava + } + + private def filter(responseIndicesNadAliases: List[(String, java.util.List[AliasMetadata])], + allowedAliases: NonEmptyList[ClusterIndexName]) = { + val matcher = PatternsMatcher.create(allowedAliases.toList.map(_.stringify)) + responseIndicesNadAliases + .map { case (indexName, aliasesList) => + val filteredAliases = matcher.filter(aliasesList.asSafeList.toSet) + (indexName, filteredAliases.toList.asJava) + } + } + +} + +object FilterableAliasesMap { + private implicit val conversion: PatternsMatcher[String]#Conversion[AliasMetadata] = Conversion.from(_.alias()) + private implicit val matchable: Matchable[String] = Matchable.caseSensitiveStringMatchable + + type AliasesMap = java.util.Map[String, java.util.List[AliasMetadata]] + + implicit def toFilterableAliasesMap(map: AliasesMap): FilterableAliasesMap = new FilterableAliasesMap(map) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryFieldsUsage.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryFieldsUsage.scala new file mode 100644 index 0000000000..d526e4f85b --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryFieldsUsage.scala @@ -0,0 +1,115 @@ +/* + * 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.queries + +import cats.data.NonEmptyList +import cats.implicits._ +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.index.query._ +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage.{CannotExtractFields, NotUsingFields, UsedField, UsingFields} +import tech.beshu.ror.es.handler.request.queries.QueryType.instances._ +import tech.beshu.ror.es.handler.request.queries.QueryType.{Compound, Leaf} +import tech.beshu.ror.utils.ReflecUtils.invokeMethodCached + +import scala.annotation.nowarn + +trait QueryFieldsUsage[QUERY <: QueryBuilder] { + def fieldsIn(query: QUERY): RequestFieldsUsage +} + +object QueryFieldsUsage extends Logging { + def apply[QUERY <: QueryBuilder](implicit ev: QueryFieldsUsage[QUERY]): QueryFieldsUsage[QUERY] = ev + + implicit class Ops[QUERY <: QueryBuilder : QueryFieldsUsage](val query: QUERY) { + def fieldsUsage: RequestFieldsUsage = QueryFieldsUsage[QUERY].fieldsIn(query) + } + + def one[QUERY <: QueryBuilder](fieldNameExtractor: QUERY => String): QueryFieldsUsage[QUERY] = + query => UsingFields(NonEmptyList.one(UsedField(fieldNameExtractor(query)))) + + def notUsing[QUERY <: QueryBuilder]: QueryFieldsUsage[QUERY] = _ => NotUsingFields + + object instances { + implicit val idsQueryFields: QueryFieldsUsage[IdsQueryBuilder] = QueryFieldsUsage.notUsing + + implicit val commonTermsQueryFields: QueryFieldsUsage[CommonTermsQueryBuilder] = QueryFieldsUsage.one(_.getWriteableName()) + implicit val matchBoolPrefixQueryFields: QueryFieldsUsage[MatchBoolPrefixQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val matchQueryFields: QueryFieldsUsage[MatchQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val matchPhraseQueryFields: QueryFieldsUsage[MatchPhraseQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val matchPhrasePrefixQueryFields: QueryFieldsUsage[MatchPhrasePrefixQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + + implicit val existsQueryFields: QueryFieldsUsage[ExistsQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val fuzzyQueryFields: QueryFieldsUsage[FuzzyQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val prefixQueryFields: QueryFieldsUsage[PrefixQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val rangeQueryFields: QueryFieldsUsage[RangeQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val regexpQueryFields: QueryFieldsUsage[RegexpQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val termQueryFields: QueryFieldsUsage[TermQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val wildcardQueryFields: QueryFieldsUsage[WildcardQueryBuilder] = QueryFieldsUsage.one(_.fieldName()) + implicit val termsSetQueryFields: QueryFieldsUsage[TermsSetQueryBuilder] = query => { + Option(invokeMethodCached(query, query.getClass, "getFieldName")) match { + case Some(fieldName: String) => UsingFields(NonEmptyList.one(UsedField(fieldName))) + case _ => + logger.debug(s"Cannot extract fields for terms set query") + CannotExtractFields + } + } + + implicit val rootQueryFields: QueryFieldsUsage[QueryBuilder] = { + //compound + case builder: BoolQueryBuilder => resolveFieldsUsageForCompoundQuery(builder) + case builder: BoostingQueryBuilder => resolveFieldsUsageForCompoundQuery(builder) + case builder: ConstantScoreQueryBuilder => resolveFieldsUsageForCompoundQuery(builder) + case builder: DisMaxQueryBuilder => resolveFieldsUsageForCompoundQuery(builder) + + //leaf + case builder: CommonTermsQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: MatchBoolPrefixQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: MatchQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: MatchPhraseQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: MatchPhrasePrefixQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: ExistsQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: FuzzyQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: PrefixQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: RangeQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: RegexpQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: TermQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: TermsSetQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder: WildcardQueryBuilder => resolveFieldsUsageForLeafQuery(builder) + case builder => + logger.debug(s"Cannot extract fields for query: ${builder.getName}") + CannotExtractFields + } + + @nowarn("cat=unused") + private def resolveFieldsUsageForLeafQuery[QUERY <: QueryBuilder : QueryFieldsUsage : Leaf](leafQuery: QUERY) = { + leafQuery.fieldsUsage + } + + private def resolveFieldsUsageForCompoundQuery[QUERY <: QueryBuilder : Compound](compoundQuery: QUERY): RequestFieldsUsage = { + val innerQueries = Compound[QUERY].innerQueriesOf(compoundQuery) + NonEmptyList.fromList(innerQueries) match { + case Some(definedInnerQueries) => + definedInnerQueries + .map(_.fieldsUsage) + .combineAll + case None => + NotUsingFields + } + } + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryType.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryType.scala new file mode 100644 index 0000000000..7c795d39e3 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryType.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.es.handler.request.queries + +import org.elasticsearch.index.query._ + +import scala.jdk.CollectionConverters._ + +sealed trait QueryType[QUERY <: QueryBuilder] + +object QueryType { + + trait Compound[QUERY <: QueryBuilder] extends QueryType[QUERY] { + def innerQueriesOf(query: QUERY): List[QueryBuilder] + } + + object Compound { + + def apply[QUERY <: QueryBuilder](implicit ev: Compound[QUERY]) = ev + + implicit class Ops[QUERY <: QueryBuilder : Compound](val query: QUERY) { + def innerQueries = Compound[QUERY].innerQueriesOf(query) + } + + def withInnerQueries[QUERY <: QueryBuilder](f: QUERY => List[QueryBuilder]) = new Compound[QUERY] { + override def innerQueriesOf(query: QUERY): List[QueryBuilder] = f(query) + } + + def oneInnerQuery[QUERY <: QueryBuilder](f: QUERY => QueryBuilder) = new Compound[QUERY] { + override def innerQueriesOf(query: QUERY): List[QueryBuilder] = List(f(query)) + } + } + + trait Leaf[QUERY <: QueryBuilder] extends QueryType[QUERY] + + object instances { + + implicit val boolQueryType: Compound[BoolQueryBuilder] = Compound.withInnerQueries { query => + List(query.must(), query.mustNot(), query.filter(), query.should()) + .flatMap(_.asScala.toList) + } + + implicit val boostingQueryType: Compound[BoostingQueryBuilder] = Compound.withInnerQueries { query => + List(query.negativeQuery(), query.positiveQuery()) + } + + implicit val disMaxQueryType: Compound[DisMaxQueryBuilder] = Compound.withInnerQueries { query => + query.innerQueries() + .asScala + .toList + } + + implicit val constantScoreQueryType: Compound[ConstantScoreQueryBuilder] = Compound.oneInnerQuery(_.innerQuery()) + + implicit object CommonTermsQueryType extends Leaf[CommonTermsQueryBuilder] + implicit object ExistsQueryType extends Leaf[ExistsQueryBuilder] + implicit object FuzzyQueryType extends Leaf[FuzzyQueryBuilder] + implicit object PrefixQueryType extends Leaf[PrefixQueryBuilder] + implicit object RangeQueryType extends Leaf[RangeQueryBuilder] + implicit object RegexpQueryType extends Leaf[RegexpQueryBuilder] + implicit object TermQueryType extends Leaf[TermQueryBuilder] + implicit object TermsSetQueryType extends Leaf[TermsSetQueryBuilder] + implicit object WildcardQueryType extends Leaf[WildcardQueryBuilder] + + implicit object MatchBoolPrefixQueryType extends Leaf[MatchBoolPrefixQueryBuilder] + implicit object MatchQueryType extends Leaf[MatchQueryBuilder] + implicit object MatchPhraseQueryType extends Leaf[MatchPhraseQueryBuilder] + implicit object MatchPhrasePrefixQueryType extends Leaf[MatchPhrasePrefixQueryBuilder] + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryWithModifiableFields.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryWithModifiableFields.scala new file mode 100644 index 0000000000..5724baa907 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/request/queries/QueryWithModifiableFields.scala @@ -0,0 +1,286 @@ +/* + * 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.queries + +import cats.data.NonEmptyList +import cats.syntax.list._ +import org.elasticsearch.index.query._ +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage.UsedField.SpecificField +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.RequestFieldsUsage.{CannotExtractFields, NotUsingFields, UsingFields} +import tech.beshu.ror.es.handler.request.queries.QueryType.{Compound, Leaf} + +import scala.annotation.nowarn +import scala.jdk.CollectionConverters._ + +trait QueryWithModifiableFields[QUERY <: QueryBuilder] { + + def handleNotAllowedFieldsIn(query: QUERY, + notAllowedFields: NonEmptyList[SpecificField]): QUERY +} + +object QueryWithModifiableFields { + + def apply[QUERY <: QueryBuilder](implicit ev: QueryWithModifiableFields[QUERY]): QueryWithModifiableFields[QUERY] = ev + + def instance[QUERY <: QueryBuilder](f: (QUERY, NonEmptyList[SpecificField]) => QUERY): QueryWithModifiableFields[QUERY] = f(_, _) + + implicit class Ops[QUERY <: QueryBuilder : QueryWithModifiableFields](val query: QUERY) { + + def handleNotAllowedFields(notAllowedFields: NonEmptyList[SpecificField]): QUERY = { + QueryWithModifiableFields[QUERY].handleNotAllowedFieldsIn(query, notAllowedFields) + } + } + + @nowarn("cat=unused") + abstract class ModifiableLeafQuery[QUERY <: QueryBuilder : Leaf : QueryFieldsUsage] extends QueryWithModifiableFields[QUERY] { + + protected def replace(query: QUERY, + notAllowedFields: NonEmptyList[SpecificField]): QUERY + + override def handleNotAllowedFieldsIn(query: QUERY, + notAllowedFields: NonEmptyList[SpecificField]): QUERY = { + QueryFieldsUsage[QUERY].fieldsIn(query) match { + case UsingFields(usedFields) => + usedFields + .collect { + case specificField: SpecificField => specificField + } + .filter(notAllowedFields.toList.contains) + .toNel match { + case Some(detectedNotAllowedFields) => + replace(query, detectedNotAllowedFields) + case None => + query + } + case CannotExtractFields | NotUsingFields => + query + } + } + } + + object ModifiableLeafQuery { + + def apply[QUERY <: QueryBuilder](implicit ev: ModifiableLeafQuery[QUERY]) = ev + + def instance[QUERY <: QueryBuilder : Leaf : QueryFieldsUsage](f: (QUERY, NonEmptyList[SpecificField]) => QUERY) = new ModifiableLeafQuery[QUERY] { + override protected def replace(query: QUERY, notAllowedFields: NonEmptyList[SpecificField]): QUERY = f(query, notAllowedFields) + } + } + + object instances { + + import QueryFieldsUsage.instances._ + import QueryType.instances._ + + //term level + implicit val existsQueryHandler: ModifiableLeafQuery[ExistsQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders + .existsQuery(notAllowedFields.head.obfuscate.value) + .boost(query.boost()) + } + + implicit val fuzzyQueryHandler: ModifiableLeafQuery[FuzzyQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders + .fuzzyQuery(notAllowedFields.head.obfuscate.value, query.value()) + .fuzziness(query.fuzziness()) + .maxExpansions(query.maxExpansions()) + .prefixLength(query.prefixLength()) + .transpositions(query.transpositions()) + .rewrite(query.rewrite()) + .boost(query.boost()) + } + + implicit val prefixQueryHandler: ModifiableLeafQuery[PrefixQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders + .prefixQuery(notAllowedFields.head.obfuscate.value, query.value()) + .rewrite(query.rewrite()) + .boost(query.boost()) + } + + implicit val rangeQueryHandler: ModifiableLeafQuery[RangeQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + val newQuery = QueryBuilders.rangeQuery(notAllowedFields.head.obfuscate.value) + .boost(query.boost()) + + Option(query.from()).foreach(lowerBound => newQuery.from(lowerBound, query.includeLower())) + Option(query.timeZone()).foreach(timezone => newQuery.timeZone(timezone)) + Option(query.relation()).foreach(relation => newQuery.relation(relation.getRelationName)) + Option(query.format()).foreach(format => newQuery.format(format)) + Option(query.to()).foreach(upperBound => newQuery.to(upperBound, query.includeUpper())) + newQuery + } + + implicit val regexpQueryHandler: ModifiableLeafQuery[RegexpQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders + .regexpQuery(notAllowedFields.head.obfuscate.value, query.value()) + .flags(query.flags()) + .maxDeterminizedStates(query.maxDeterminizedStates()) + .rewrite(query.rewrite()) + .boost(query.boost()) + } + + implicit val termQueryHandler: ModifiableLeafQuery[TermQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders + .termQuery(notAllowedFields.head.obfuscate.value, query.value()) + .boost(query.boost()) + } + + implicit val termsSetQueryHandler: ModifiableLeafQuery[TermsSetQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + val newQuery = new TermsSetQueryBuilder(notAllowedFields.head.obfuscate.value, query.getValues) + + Option(query.getMinimumShouldMatchField) match { + case Some(definedMatchField) => newQuery.setMinimumShouldMatchField(definedMatchField) + case None => newQuery.setMinimumShouldMatchScript(query.getMinimumShouldMatchScript) + } + } + + implicit val wildcardQueryHandler: ModifiableLeafQuery[WildcardQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders + .wildcardQuery(notAllowedFields.head.obfuscate.value, query.value()) + .rewrite(query.rewrite()) + .boost(query.boost()) + + } + + implicit val matchBoolPrefixQueryHandler: ModifiableLeafQuery[MatchBoolPrefixQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + val newQuery = new MatchBoolPrefixQueryBuilder(notAllowedFields.head.obfuscate.value, query.value()) + .analyzer(query.analyzer()) + .minimumShouldMatch(query.minimumShouldMatch()) + .fuzzyRewrite(query.fuzzyRewrite()) + .fuzzyTranspositions(query.fuzzyTranspositions()) + .maxExpansions(query.maxExpansions()) + .operator(query.operator()) + .prefixLength(query.prefixLength()) + .boost(query.boost()) + + Option(query.fuzziness()).foreach(newQuery.fuzziness) + newQuery + } + + implicit val matchQueryHandler: ModifiableLeafQuery[MatchQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders + .matchQuery(notAllowedFields.head.obfuscate.value, query.value()) + .boost(query.boost()) + } + + implicit val matchPhraseQueryHandler: ModifiableLeafQuery[MatchPhraseQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders.matchPhraseQuery(notAllowedFields.head.obfuscate.value, query.value()) + .analyzer(query.analyzer()) + .zeroTermsQuery(query.zeroTermsQuery()) + .slop(query.slop()) + .boost(query.boost()) + } + + implicit val matchPhrasePrefixQueryHandler: ModifiableLeafQuery[MatchPhrasePrefixQueryBuilder] = ModifiableLeafQuery.instance { (query, notAllowedFields) => + QueryBuilders.matchPhrasePrefixQuery(notAllowedFields.head.obfuscate.value, query.value()) + .analyzer(query.analyzer()) + .maxExpansions(query.maxExpansions()) + .slop(query.slop()) + .boost(query.boost()) + } + + //compound + implicit val boolQueryHandler: QueryWithModifiableFields[BoolQueryBuilder] = QueryWithModifiableFields.instance { (query, notAllowedFields) => + final case class ClauseHandler(extractor: BoolQueryBuilder => java.util.List[QueryBuilder], + creator: (BoolQueryBuilder, QueryBuilder) => BoolQueryBuilder) + + def handleNotAllowedFieldsInClause(boolClauseExtractor: BoolQueryBuilder => java.util.List[QueryBuilder]) = { + boolClauseExtractor(query).asScala.map(_.handleNotAllowedFields(notAllowedFields)) + } + + val clauseHandlers = List( + ClauseHandler(_.must(), _ must _), + ClauseHandler(_.mustNot(), _ must _), + ClauseHandler(_.filter(), _ filter _), + ClauseHandler(_.should(), _ should _) + ) + + val boolQueryWithNewClauses = clauseHandlers + .map(clause => (handleNotAllowedFieldsInClause(clause.extractor), clause.creator)) + .foldLeft(QueryBuilders.boolQuery()) { + case (modifiedBoolQuery, (modifiedClauses, clauseCreator)) => + modifiedClauses.foldLeft(modifiedBoolQuery)(clauseCreator) + } + + boolQueryWithNewClauses + .minimumShouldMatch(query.minimumShouldMatch()) + .adjustPureNegative(query.adjustPureNegative()) + .boost(query.boost()) + } + + implicit val boostingQueryHandler: QueryWithModifiableFields[BoostingQueryBuilder] = QueryWithModifiableFields.instance { (query, notAllowedFields) => + val newPositiveQuery = query.positiveQuery().handleNotAllowedFields(notAllowedFields) + val newNegativeQuery = query.negativeQuery().handleNotAllowedFields(notAllowedFields) + + QueryBuilders + .boostingQuery(newPositiveQuery, newNegativeQuery) + .negativeBoost(query.negativeBoost()) + .boost(query.boost()) + } + + implicit val constantScoreQueryHandler: QueryWithModifiableFields[ConstantScoreQueryBuilder] = QueryWithModifiableFields.instance { (query, notAllowedFields) => + val newInnerQuery = query.innerQuery().handleNotAllowedFields(notAllowedFields) + + QueryBuilders + .constantScoreQuery(newInnerQuery) + .boost(query.boost()) + } + + implicit val disjuctionMaxQueryHandler: QueryWithModifiableFields[DisMaxQueryBuilder] = QueryWithModifiableFields.instance { (query, notAllowedFields) => + query.innerQueries().asScala + .map(_.handleNotAllowedFields(notAllowedFields)) + .foldLeft(QueryBuilders.disMaxQuery())(_ add _) + .tieBreaker(query.tieBreaker()) + .boost(query.tieBreaker()) + } + + implicit val rootQueryHandler: QueryWithModifiableFields[QueryBuilder] = (query: QueryBuilder, notAllowedFields: NonEmptyList[SpecificField]) => query match { + case builder: BoolQueryBuilder => handleCompoundQuery(builder, notAllowedFields) + case builder: BoostingQueryBuilder => handleCompoundQuery(builder, notAllowedFields) + case builder: ConstantScoreQueryBuilder => handleCompoundQuery(builder, notAllowedFields) + case builder: DisMaxQueryBuilder => handleCompoundQuery(builder, notAllowedFields) + + //fulltext + case builder: MatchBoolPrefixQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: MatchQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: MatchPhraseQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: MatchPhrasePrefixQueryBuilder => handleLeafQuery(builder, notAllowedFields) + + //termlevel + case builder: ExistsQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: FuzzyQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: PrefixQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: RangeQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: RegexpQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: TermQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: TermsSetQueryBuilder => handleLeafQuery(builder, notAllowedFields) + case builder: WildcardQueryBuilder => handleLeafQuery(builder, notAllowedFields) + + case other => other + } + + private def handleLeafQuery[QUERY <: QueryBuilder : ModifiableLeafQuery](leafQuery: QUERY, + notAllowedFields: NonEmptyList[SpecificField]) = { + ModifiableLeafQuery[QUERY].handleNotAllowedFieldsIn(leafQuery, notAllowedFields) + } + + @nowarn("cat=unused") + private def handleCompoundQuery[QUERY <: QueryBuilder : Compound : QueryWithModifiableFields](compoundQuery: QUERY, + notAllowedFields: NonEmptyList[SpecificField]) = { + QueryWithModifiableFields[QUERY].handleNotAllowedFieldsIn(compoundQuery, notAllowedFields) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/response/DocumentApiOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/DocumentApiOps.scala new file mode 100644 index 0000000000..d40a2b3b0a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/DocumentApiOps.scala @@ -0,0 +1,116 @@ +/* + * 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.response + +import org.elasticsearch.action.get.{GetResponse, MultiGetItemResponse} +import org.elasticsearch.action.index.IndexRequest +import org.elasticsearch.index.get.GetResult +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions +import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, DocumentId, DocumentWithIndex} +import tech.beshu.ror.es.handler.RequestSeemsToBeInvalid +import tech.beshu.ror.utils.ReflecUtils +import scala.jdk.CollectionConverters._ + +object DocumentApiOps { + + object GetApi { + + //it's ugly but I don't know better way to do it + def doesNotExistResponse(original: GetResponse): GetResponse = { + val exists = false + val source = null + val result = new GetResult( + original.getIndex, + original.getId, + original.getSeqNo, + original.getPrimaryTerm, + original.getVersion, + exists, + source, + java.util.Collections.emptyMap(), + java.util.Collections.emptyMap()) + new GetResponse(result) + } + + implicit class GetResponseOps(val response: GetResponse) extends AnyVal { + def asDocumentWithIndex: DocumentWithIndex = createDocumentWithIndex(response.getIndex, response.getId) + + def filterFieldsUsing(fieldsRestrictions: FieldsRestrictions): GetResponse = { + val newSource = filterSourceFieldsUsing(fieldsRestrictions) + val newFields = filterDocumentFieldsUsing(fieldsRestrictions) + + val newResult = new GetResult( + response.getIndex, + response.getId, + response.getSeqNo, + response.getPrimaryTerm, + response.getVersion, + true, + newSource, + newFields.nonMetadataDocumentFields.value.asJava, + newFields.metadataDocumentFields.value.asJava + ) + new GetResponse(newResult) + } + + private def filterSourceFieldsUsing(fieldsRestrictions: FieldsRestrictions) = { + Option(response.getSourceAsMap) + .map(_.asScala.toMap) + .filter(_.nonEmpty) + .map(source => FieldsFiltering.filterSource(source, fieldsRestrictions)) match { + case Some(value) => value.bytes + case None => response.getSourceAsBytesRef + } + } + + private def filterDocumentFieldsUsing(fieldsRestrictions: FieldsRestrictions) = { + Option(ReflecUtils.getField(response, response.getClass, "getResult")) + .collect { + case getResult: GetResult => getResult + } + .map { getResult => + val originalNonMetadataFields = FieldsFiltering.NonMetadataDocumentFields(getResult.getDocumentFields.asScala.toMap) + val originalMetadataFields = FieldsFiltering.MetadataDocumentFields(getResult.getMetadataFields.asScala.toMap) + + val filteredNonMetadataFields = FieldsFiltering.filterNonMetadataDocumentFields(originalNonMetadataFields, fieldsRestrictions) + FieldsFiltering.NewFilteredDocumentFields(filteredNonMetadataFields, originalMetadataFields) + } + .getOrElse(throw new IllegalStateException("Could not access get result in get response.")) + } + } + } + + object MultiGetApi { + implicit class MultiGetItemResponseOps(val item: MultiGetItemResponse) extends AnyVal { + def asDocumentWithIndex: DocumentWithIndex = createDocumentWithIndex(item.getIndex, item.getId) + } + } + + private def createDocumentWithIndex(indexStr: String, docId: String) = { + val indexName = createIndexName(indexStr) + val documentId = DocumentId(docId) + DocumentWithIndex(indexName, documentId) + } + + private def createIndexName(indexStr: String) = { + ClusterIndexName + .fromString(indexStr) + .getOrElse { + throw RequestSeemsToBeInvalid[IndexRequest]("Index name is invalid") + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/response/FLSContextHeaderHandler.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/FLSContextHeaderHandler.scala new file mode 100644 index 0000000000..9ff6c0a2da --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/FLSContextHeaderHandler.scala @@ -0,0 +1,49 @@ +/* + * 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.response + +import cats.implicits._ +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.threadpool.ThreadPool +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions +import tech.beshu.ror.accesscontrol.domain.Header +import tech.beshu.ror.accesscontrol.domain.Header.Name +import tech.beshu.ror.accesscontrol.headerValues.transientFieldsToHeaderValue +import tech.beshu.ror.accesscontrol.request.RequestContext + +object FLSContextHeaderHandler extends Logging { + + def addContextHeader(threadPool: ThreadPool, + fieldsRestrictions: FieldsRestrictions, + requestId: RequestContext.Id): Unit = { + val threadContext = threadPool.getThreadContext + val header = createContextHeader(fieldsRestrictions) + Option(threadContext.getHeader(header.name.value.value)) match { + case None => + logger.debug(s"[${requestId.show}] Adding thread context header required by lucene. Header Value: '${header.value.value}'") + threadContext.putHeader(header.name.value.value, header.value.value) + case Some(_) => + } + } + + private def createContextHeader(fieldsRestrictions: FieldsRestrictions) = { + new Header( + Name.transientFields, + transientFieldsToHeaderValue.toRawValue(fieldsRestrictions) + ) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/response/FieldsFiltering.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/FieldsFiltering.scala new file mode 100644 index 0000000000..4dc99c6588 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/FieldsFiltering.scala @@ -0,0 +1,63 @@ +/* + * 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.response + +import org.elasticsearch.common.bytes.BytesReference +import org.elasticsearch.common.xcontent.support.XContentMapValues +import org.elasticsearch.xcontent.{XContentFactory, XContentType} +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions.AccessMode +import tech.beshu.ror.fls.FieldsPolicy +import scala.jdk.CollectionConverters._ + +object FieldsFiltering { + + final case class NewFilteredSource(bytes: BytesReference) + + final case class NonMetadataDocumentFields[T](value: Map[String, T]) + final case class MetadataDocumentFields[T](value: Map[String, T]) + + final case class NewFilteredDocumentFields[T](nonMetadataDocumentFields: NonMetadataDocumentFields[T], + metadataDocumentFields: MetadataDocumentFields[T]) + + def filterSource(sourceAsMap: Map[String, AnyRef], + fieldsRestrictions: FieldsRestrictions): NewFilteredSource = { + val (excluding, including) = splitFieldsByAccessMode(fieldsRestrictions) + val filteredSource = XContentMapValues.filter(sourceAsMap.asJava, including.toArray, excluding.toArray) + val newContent = XContentFactory + .contentBuilder(XContentType.JSON) + .map(filteredSource) + NewFilteredSource(BytesReference.bytes(newContent)) + } + + def filterNonMetadataDocumentFields[T](nonMetadataDocumentFields: NonMetadataDocumentFields[T], + fieldsRestrictions: FieldsRestrictions): NonMetadataDocumentFields[T] = { + val policy = new FieldsPolicy(fieldsRestrictions) + + NonMetadataDocumentFields { + nonMetadataDocumentFields.value.filter { + case (key, _) => + policy.canKeep(key) + } + } + } + + private def splitFieldsByAccessMode(fields: FieldsRestrictions) = fields.mode match { + case AccessMode.Whitelist => (List.empty, fields.documentFields.map(_.value.value).toList) + case AccessMode.Blacklist => (fields.documentFields.map(_.value.value).toList, List.empty) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/response/ForbiddenResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/ForbiddenResponse.scala new file mode 100644 index 0000000000..7eaeaa7fae --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/ForbiddenResponse.scala @@ -0,0 +1,99 @@ +/* + * 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.response + +import cats.Show +import cats.implicits._ +import org.elasticsearch.ElasticsearchException +import org.elasticsearch.rest.RestStatus +import tech.beshu.ror.accesscontrol.AccessControl.{AccessControlStaticContext, ForbiddenCause} +import tech.beshu.ror.accesscontrol.factory.GlobalSettings + +import scala.jdk.CollectionConverters._ + +class ForbiddenResponse private(aclStaticContext: Option[AccessControlStaticContext], + causes: List[ForbiddenResponse.Cause]) + extends ElasticsearchException( + aclStaticContext.map(_.forbiddenRequestMessage).getOrElse(GlobalSettings.defaultForbiddenRequestMessage) + ) { + + import ForbiddenResponse.forbiddenCauseShow + + addMetadata("es.due_to", causes.map(_.show).asJava) + + aclStaticContext match { + case Some(context) if context.doesRequirePassword => + addHeader("WWW-Authenticate", "Basic") + case _ => + } + + override def status(): RestStatus = aclStaticContext match { + case Some(context) if context.doesRequirePassword => + RestStatus.UNAUTHORIZED + case _ => + RestStatus.FORBIDDEN + } +} + +object ForbiddenResponse { + + sealed trait Cause + object Cause { + def fromMismatchedCause(cause: ForbiddenCause): Cause = { + cause match { + case ForbiddenCause.OperationNotAllowed => OperationNotAllowed + case ForbiddenCause.ImpersonationNotSupported => ImpersonationNotSupported + case ForbiddenCause.ImpersonationNotAllowed => ImpersonationNotAllowed + } + } + } + case object ForbiddenBlockMatch extends Cause + case object OperationNotAllowed extends Cause + case object ImpersonationNotSupported extends Cause + case object ImpersonationNotAllowed extends Cause + case object RorNotReadyYet extends Cause + case object RorNotEnabled extends Cause + case object RorFailedToStart extends Cause + case object TestSettingsNotConfigured extends Cause + + def create(causes: List[ForbiddenResponse.Cause], + aclStaticContext: AccessControlStaticContext): ForbiddenResponse = + new ForbiddenResponse(Some(aclStaticContext), causes) + + def createRorStartingFailureResponse(): ForbiddenResponse = + new ForbiddenResponse(None, RorFailedToStart :: Nil) + + def createRorNotReadyYetResponse(): ForbiddenResponse = + new ForbiddenResponse(None, RorNotReadyYet :: Nil) + + def createRorNotEnabledResponse(): ForbiddenResponse = + new ForbiddenResponse(None, RorNotEnabled :: Nil) + + def createTestSettingsNotConfiguredResponse(): ForbiddenResponse = + new ForbiddenResponse(None, TestSettingsNotConfigured :: Nil) + + private implicit val forbiddenCauseShow: Show[Cause] = Show.show { + case ForbiddenBlockMatch => "FORBIDDEN_BY_BLOCK" + case OperationNotAllowed => "OPERATION_NOT_ALLOWED" + case ImpersonationNotSupported => "IMPERSONATION_NOT_SUPPORTED" + case ImpersonationNotAllowed => "IMPERSONATION_NOT_ALLOWED" + case RorNotReadyYet => "READONLYREST_NOT_READY_YET" + case RorNotEnabled => "READONLYREST_NOT_ENABLED" + case RorFailedToStart => "READONLYREST_FAILED_TO_START" + case TestSettingsNotConfigured => "TEST_SETTINGS_NOT_CONFIGURED" + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/response/SearchHitOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/SearchHitOps.scala new file mode 100644 index 0000000000..d0b993ff1a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/SearchHitOps.scala @@ -0,0 +1,65 @@ +/* + * 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.response + +import java.util + +import org.elasticsearch.common.document.DocumentField +import org.elasticsearch.search.SearchHit +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions +import tech.beshu.ror.utils.ReflecUtils + +import scala.util.Try +import scala.jdk.CollectionConverters._ + +object SearchHitOps { + + implicit class Filtering(val searchHit: SearchHit) extends AnyVal { + + def filterSourceFieldsUsing(fieldsRestrictions: FieldsRestrictions): SearchHit = { + Option(searchHit.getSourceAsMap) + .map(_.asScala.toMap) + .filter(_.nonEmpty) + .map(source => FieldsFiltering.filterSource(source, fieldsRestrictions)) + .foreach(newSource => searchHit.sourceRef(newSource.bytes)) + + searchHit + } + + def filterDocumentFieldsUsing(fieldsRestrictions: FieldsRestrictions): SearchHit = { + val documentFields = extractDocumentFields(searchHit) + + Option(documentFields) + .map(fields => FieldsFiltering.NonMetadataDocumentFields(fields.asScala.toMap)) + .filter(_.value.nonEmpty) + .map(fields => FieldsFiltering.filterNonMetadataDocumentFields(fields, fieldsRestrictions)) + .map(_.value) + .foreach { newDocumentFields => + ReflecUtils.setField(searchHit, searchHit.getClass, "documentFields", newDocumentFields.asJava) + } + searchHit + } + } + + private def extractDocumentFields(searchHit: SearchHit) = { + Try { + ReflecUtils.getField(searchHit, searchHit.getClass, "documentFields") + .asInstanceOf[util.Map[String, DocumentField]] + } + .getOrElse(throw new IllegalStateException("Could not access document fields in search hit.")) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/handler/response/ServiceNotAvailableResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/ServiceNotAvailableResponse.scala new file mode 100644 index 0000000000..647a500a7b --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/handler/response/ServiceNotAvailableResponse.scala @@ -0,0 +1,53 @@ +/* + * 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.response + +import cats.Show +import cats.implicits.toShow +import org.elasticsearch.ElasticsearchException +import org.elasticsearch.rest.RestStatus +import tech.beshu.ror.accesscontrol.factory.GlobalSettings + +import scala.jdk.CollectionConverters._ + +class ServiceNotAvailableResponse private(cause: ServiceNotAvailableResponse.Cause) + extends ElasticsearchException(GlobalSettings.defaultForbiddenRequestMessage) { + + addMetadata("es.due_to", List(cause).map(_.show).asJava) + + override def status(): RestStatus = RestStatus.SERVICE_UNAVAILABLE +} + +object ServiceNotAvailableResponse { + + sealed trait Cause + object Cause { + case object RorNotReadyYet extends Cause + case object RorFailedToStart extends Cause + } + + private implicit val causeShow: Show[Cause] = Show.show { + case Cause.RorNotReadyYet => "READONLYREST_NOT_READY_YET" + case Cause.RorFailedToStart => "READONLYREST_FAILED_TO_START" + } + + def createRorStartingFailureResponse(): ServiceNotAvailableResponse = + new ServiceNotAvailableResponse(Cause.RorFailedToStart) + + def createRorNotReadyYetResponse(): ServiceNotAvailableResponse = + new ServiceNotAvailableResponse(Cause.RorNotReadyYet) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/services/EsAuditSinkService.scala b/es813x/src/main/scala/tech/beshu/ror/es/services/EsAuditSinkService.scala new file mode 100644 index 0000000000..ddc07363d0 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/services/EsAuditSinkService.scala @@ -0,0 +1,88 @@ +/* + * 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.services + +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.bulk.{BackoffPolicy, BulkProcessor, BulkRequest, BulkResponse} +import org.elasticsearch.action.index.IndexRequest +import org.elasticsearch.client.internal.Client +import org.elasticsearch.common.inject.Inject +import org.elasticsearch.common.unit.{ByteSizeUnit, ByteSizeValue} +import org.elasticsearch.core.TimeValue +import org.elasticsearch.xcontent.XContentType +import tech.beshu.ror.constants.{AUDIT_SINK_MAX_ITEMS, AUDIT_SINK_MAX_KB, AUDIT_SINK_MAX_RETRIES, AUDIT_SINK_MAX_SECONDS} +import tech.beshu.ror.es.AuditSinkService + +import java.util.function.BiConsumer + +@Inject +class EsAuditSinkService(client: Client) + extends AuditSinkService + with Logging { + + private val bulkProcessor = + BulkProcessor + .builder(BulkRequestHandler, new AuditSinkBulkProcessorListener, "ror-audit-bulk-processor") + .setBulkActions(AUDIT_SINK_MAX_ITEMS) + .setBulkSize(new ByteSizeValue(AUDIT_SINK_MAX_KB, ByteSizeUnit.KB)) + .setFlushInterval(TimeValue.timeValueSeconds(AUDIT_SINK_MAX_SECONDS)) + .setConcurrentRequests(1) + .setBackoffPolicy(BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(100), AUDIT_SINK_MAX_RETRIES)) + .build + + override def submit(indexName: String, documentId: String, jsonRecord: String): Unit = { + bulkProcessor.add( + new IndexRequest(indexName) + .id(documentId) + .source(jsonRecord, XContentType.JSON) + ) + } + + override def close(): Unit = { + bulkProcessor.close() + } + + private object BulkRequestHandler extends BiConsumer[BulkRequest, ActionListener[BulkResponse]] { + override def accept(t: BulkRequest, u: ActionListener[BulkResponse]): Unit = client.bulk(t, u) + } + + private class AuditSinkBulkProcessorListener extends BulkProcessor.Listener { + override def beforeBulk(executionId: Long, request: BulkRequest): Unit = { + logger.debug(s"Flushing ${request.numberOfActions} bulk actions ...") + } + + override def afterBulk(executionId: Long, request: BulkRequest, response: BulkResponse): Unit = { + if (response.hasFailures) { + logger.error("Some failures flushing the BulkProcessor: ") + response + .getItems.to(LazyList) + .filter(_.isFailed) + .map(_.getFailureMessage) + .groupBy(identity) + .foreach { case (message, stream) => + logger.error(s"${stream.size}x: $message") + } + } + } + + override def afterBulk(executionId: Long, request: BulkRequest, failure: Throwable): Unit = { + logger.error(s"Failed flushing the BulkProcessor: ${failure.getMessage}", failure) + } + } + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/services/EsIndexJsonContentService.scala b/es813x/src/main/scala/tech/beshu/ror/es/services/EsIndexJsonContentService.scala new file mode 100644 index 0000000000..5dd81c451f --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/services/EsIndexJsonContentService.scala @@ -0,0 +1,122 @@ +/* + * 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.services + +import cats.implicits._ +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.ResourceNotFoundException +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.common.inject.{Inject, Singleton} +import org.elasticsearch.xcontent.XContentType +import tech.beshu.ror.accesscontrol.domain.IndexName +import tech.beshu.ror.accesscontrol.show.logs._ +import tech.beshu.ror.boot.RorSchedulers +import tech.beshu.ror.es.IndexJsonContentService +import tech.beshu.ror.es.IndexJsonContentService._ +import tech.beshu.ror.utils.ScalaOps._ + +import scala.annotation.nowarn +import scala.jdk.CollectionConverters._ + +@Singleton +class EsIndexJsonContentService(client: NodeClient, + @nowarn("cat=unused") constructorDiscriminator: Unit) + extends IndexJsonContentService + with Logging { + + @Inject + def this(client: NodeClient) = { + this(client, ()) + } + + override def sourceOf(index: IndexName.Full, + id: String): Task[Either[ReadError, Map[String, String]]] = { + Task { + client + .get( + client + .prepareGet() + .setIndex(index.name.value) + .setId(id) + .request() + ) + .actionGet() + } + .map { response => + if (response.isExists) { + Option(response.getSourceAsMap) match { + case Some(map) => + val source = map.asScala.toMap.asStringMap + logger.debug(s"Document [${index.show} ID=$id] _source: ${showSource(source)}") + Right(source) + case None => + logger.warn(s"Document [${index.show} ID=$id] _source is not available. Assuming it's empty") + Right(Map.empty[String, String]) + } + } else { + logger.debug(s"Document [${index.show} ID=$id] not exist") + Left(ContentNotFound) + } + } + .executeOn(RorSchedulers.blockingScheduler) + .onErrorRecover { + case _: ResourceNotFoundException => Left(ContentNotFound) + case ex => + logger.error(s"Cannot get source of document [${index.show} ID=$id]", ex) + Left(CannotReachContentSource) + } + } + + override def saveContent(index: IndexName.Full, + id: String, + content: Map[String, String]): Task[Either[WriteError, Unit]] = { + Task { + client + .index( + client + .prepareIndex() + .setIndex(index.name.value) + .setId(id) + .setSource(content.asJava, XContentType.JSON) + .setRefreshPolicy(RefreshPolicy.WAIT_UNTIL) + .request() + ) + .actionGet() + } + .map { response => + response.status().getStatus match { + case status if status / 100 == 2 => + Right(()) + case status => + logger.error(s"Cannot write to document [${index.show} ID=$id]. Unexpected response: HTTP $status, response: ${response.toString}") + Left(CannotWriteToIndex) + } + } + .executeOn(RorSchedulers.blockingScheduler) + .onErrorRecover { + case ex => + logger.error(s"Cannot write to document [${index.show} ID=$id]", ex) + Left(CannotWriteToIndex) + } + } + + private def showSource(source: Map[String, String]) = { + ujson.write(source) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/services/EsServerBasedRorClusterService.scala b/es813x/src/main/scala/tech/beshu/ror/es/services/EsServerBasedRorClusterService.scala new file mode 100644 index 0000000000..d88837196f --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/services/EsServerBasedRorClusterService.scala @@ -0,0 +1,501 @@ +/* + * 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.services + +import cats.data.NonEmptyList +import cats.implicits._ +import cats.kernel.Monoid +import eu.timepit.refined.types.string.NonEmptyString +import monix.eval.Task +import monix.execution.{CancelablePromise, Scheduler} +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.action.ActionListener +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction.{ResolvedAlias, ResolvedIndex} +import org.elasticsearch.action.search.{MultiSearchResponse, SearchRequestBuilder, SearchResponse} +import org.elasticsearch.action.support.PlainActionFuture +import org.elasticsearch.client.internal.RemoteClusterClient +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.cluster.metadata.{IndexMetadata, Metadata, RepositoriesMetadata} +import org.elasticsearch.cluster.service.ClusterService +import org.elasticsearch.index.query.QueryBuilders +import org.elasticsearch.repositories.{RepositoriesService, RepositoryData} +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.RemoteClusterService +import tech.beshu.ror.accesscontrol.domain.ClusterIndexName.Remote.ClusterName +import tech.beshu.ror.accesscontrol.domain.DataStreamName.{FullLocalDataStreamWithAliases, FullRemoteDataStreamWithAliases} +import tech.beshu.ror.accesscontrol.domain.DocumentAccessibility.{Accessible, Inaccessible} +import tech.beshu.ror.accesscontrol.domain._ +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.accesscontrol.show.logs._ +import tech.beshu.ror.es.RorClusterService +import tech.beshu.ror.es.RorClusterService._ +import tech.beshu.ror.es.utils.CallActionRequestAndHandleResponse._ +import tech.beshu.ror.utils.ScalaOps._ +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import java.util.function.Supplier +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +class EsServerBasedRorClusterService(nodeName: String, + clusterService: ClusterService, + remoteClusterServiceSupplier: Supplier[Option[RemoteClusterService]], + repositoriesServiceSupplier: Supplier[Option[RepositoriesService]], + nodeClient: NodeClient, + threadPool: ThreadPool) + (implicit val scheduler: Scheduler) + extends RorClusterService + with Logging { + + override def indexOrAliasUuids(indexOrAlias: IndexOrAlias): Set[IndexUuid] = { + val lookup = clusterService.state.metadata.getIndicesLookup + lookup.get(indexOrAlias.stringify).getIndices.asScala.map(_.getUUID).toSet + } + + override def allIndicesAndAliases: Set[FullLocalIndexWithAliases] = { + val metadata = clusterService.state.metadata + extractIndicesAndAliasesFrom(metadata) + } + + override def allRemoteIndicesAndAliases: Task[Set[FullRemoteIndexWithAliases]] = { + remoteClusterServiceSupplier.get() match { + case Some(remoteClusterService) => + provideAllRemoteIndices(remoteClusterService) + case None => + Task.now(Set.empty) + } + } + + override def allDataStreamsAndAliases: Set[FullLocalDataStreamWithAliases] = { + val metadata = clusterService.state.metadata + extractDataStreamsAndAliases(metadata) + } + + override def allRemoteDataStreamsAndAliases: Task[Set[FullRemoteDataStreamWithAliases]] = + remoteClusterServiceSupplier.get() match { + case Some(remoteClusterService) => + provideAllRemoteDataStreams(remoteClusterService) + case None => + Task.now(Set.empty) + } + + override def allTemplates: Set[Template] = { + legacyTemplates() ++ indexTemplates() ++ componentTemplates() + } + + override def allSnapshots: Map[RepositoryName.Full, Set[SnapshotName.Full]] = { + val repositoriesMetadata: RepositoriesMetadata = clusterService.state().metadata().custom(RepositoriesMetadata.TYPE) + repositoriesMetadata + .repositories().asSafeList + .flatMap { repositoryMetadata => + RepositoryName + .from(repositoryMetadata.name()) + .flatMap { + case r: RepositoryName.Full => Some(r) + case _ => None + } + .map { name => (name, snapshotsBy(name)) } + } + .toMap + } + + override def verifyDocumentAccessibility(document: Document, + filter: Filter, + id: RequestContext.Id): Task[DocumentAccessibility] = { + createSearchRequest(filter, document) + .call(extractAccessibilityFrom) + .onErrorRecover { + case ex => + logger.error(s"[${id.show}] Could not verify get request. Blocking document", ex) + Inaccessible + } + } + + override def verifyDocumentsAccessibilities(documents: NonEmptyList[Document], + filter: Filter, + id: RequestContext.Id): Task[DocumentsAccessibilities] = { + createMultiSearchRequest(filter, documents) + .call(extractResultsFromSearchResponse) + .onErrorRecover { + case ex => + logger.error(s"[${id.show}] Could not verify documents returned by multi get response. Blocking all returned documents", ex) + blockAllDocsReturned(documents) + } + .map(results => zip(results, documents)) + } + + private def extractIndicesAndAliasesFrom(metadata: Metadata) = { + val indices = metadata.getIndices + indices + .keySet().asScala + .flatMap { index => + val indexMetaData = indices.get(index) + IndexName.Full + .fromString(indexMetaData.getIndex.getName) + .map { indexName => + val aliases = indexMetaData.getAliases.asSafeMap.keys.flatMap(IndexName.Full.fromString).toSet + FullLocalIndexWithAliases( + indexName, + indexMetaData.getState match { + case IndexMetadata.State.CLOSE => IndexAttribute.Closed + case IndexMetadata.State.OPEN => IndexAttribute.Opened + }, + aliases + ) + } + } + .toSet + } + + private def extractDataStreamsAndAliases(metadata: Metadata): Set[FullLocalDataStreamWithAliases] = { + val aliasesPerDataStream = aliasesPerDataStreamFrom(metadata) + backingIndicesPerDataStreamFrom(metadata) + .map { case (dataStreamName, backingIndices) => + FullLocalDataStreamWithAliases( + dataStreamName = dataStreamName, + aliasesNames = aliasesPerDataStream.getOrElse(dataStreamName, Set.empty), + backingIndices = backingIndices + ) + } + .toSet + } + + private def aliasesPerDataStreamFrom(metadata: Metadata): Map[DataStreamName.Full, Set[DataStreamName.Full]] = { + lazy val mapMonoid: Monoid[Map[DataStreamName.Full, Set[DataStreamName.Full]]] = + Monoid[Map[DataStreamName.Full, Set[DataStreamName.Full]]] + val dataStreamAliases = metadata.dataStreamAliases() + dataStreamAliases + .keySet().asScala + .flatMap { aliasName => + val dataStreamAlias = dataStreamAliases.get(aliasName) + val dataStreams: Set[DataStreamName.Full] = + dataStreamAlias + .getDataStreams.asScala + .flatMap { ds => + DataStreamName.Full.fromString(ds) + } + .toSet + + DataStreamName.Full.fromString(dataStreamAlias.getName) + .map(alias => (alias, dataStreams)) + } + .map { + case (alias, dataStreams) => + dataStreams.map(ds => (ds, Set(alias))).toMap + } + .foldLeft(Map.empty[DataStreamName.Full, Set[DataStreamName.Full]]) { (acc, aliasesPerDataStream) => + mapMonoid.combine(acc, aliasesPerDataStream) + } + } + + private def backingIndicesPerDataStreamFrom(metadata: Metadata): Map[DataStreamName.Full, Set[IndexName.Full]] = { + val dataStreams = metadata.dataStreams() + dataStreams + .keySet().asScala + .flatMap { dataStreamName => + val dataStream = dataStreams.get(dataStreamName) + val backingIndices = + dataStream + .getIndices.asScala + .map(_.getName) + .flatMap( + IndexName.Full.fromString + ) + .toSet + + DataStreamName.Full + .fromString(dataStream.getName) + .map(dataStreamName => (dataStreamName, backingIndices)) + } + .toMap + } + + private def provideAllRemoteDataStreams(remoteClusterService: RemoteClusterService) = { + val remoteClusterFullNames = + remoteClusterService + .getRegisteredRemoteClusterNames.asSafeSet + .flatMap(ClusterName.Full.fromString) + + Task + .parSequenceUnordered( + remoteClusterFullNames.map(resolveAllRemoteDataStreams(_, remoteClusterService)) + ) + .map(_.flatten.toSet) + } + + private def resolveAllRemoteDataStreams(remoteClusterName: ClusterName.Full, + remoteClusterService: RemoteClusterService): Task[List[FullRemoteDataStreamWithAliases]] = { + getRemoteClusterClient(remoteClusterService, remoteClusterName) match { + case Failure(_) => + logger.error(s"Cannot get remote cluster client for remote cluster with name: ${remoteClusterName.show}") + Task.now(List.empty) + case Success(client) => + resolveRemoteIndicesUsing(client) + .map { response => + remoteDataStreamsFrom(response, remoteClusterName) + } + } + } + + private def remoteDataStreamsFrom(response: ResolveIndexAction.Response, + remoteClusterName: ClusterName.Full): List[FullRemoteDataStreamWithAliases] = { + val aliasesPerIndex: Map[IndexName.Full, Set[IndexName.Full]] = aliasesPerIndexFrom(response.getAliases.asSafeList) + response + .getDataStreams.asSafeList + .flatMap { resolvedDataStream => + IndexName.Full.fromString(resolvedDataStream.getName) + .map { dataStreamName => + val backingIndices = + resolvedDataStream + .getBackingIndices.asSafeList + .flatMap(IndexName.Full.fromString) + .toSet + + val dataStreamAliases = + aliasesPerIndex + .getOrElse(dataStreamName, Set.empty) + .map(index => DataStreamName.Full(index.name)) + + FullRemoteDataStreamWithAliases( + clusterName = remoteClusterName, + dataStreamName = DataStreamName.Full(dataStreamName.name), + aliasesNames = dataStreamAliases, + backingIndices = backingIndices + ) + } + } + } + + private def provideAllRemoteIndices(remoteClusterService: RemoteClusterService) = { + val remoteClusterFullNames = + remoteClusterService + .getRegisteredRemoteClusterNames.asSafeSet + .flatMap(ClusterName.Full.fromString) + + Task + .parSequenceUnordered( + remoteClusterFullNames.map(resolveAllRemoteIndices(_, remoteClusterService)) + ) + .map(_.flatten.toSet) + } + + private def resolveAllRemoteIndices(remoteClusterName: ClusterName.Full, + remoteClusterService: RemoteClusterService) = { + getRemoteClusterClient(remoteClusterService, remoteClusterName) match { + case Failure(_) => + logger.error(s"Cannot get remote cluster client for remote cluster with name: ${remoteClusterName.show}") + Task.now(List.empty) + case Success(client) => + resolveRemoteIndicesUsing(client) + .map { response => + response + .getIndices.asSafeList + .flatMap { resolvedIndex => + toFullRemoteIndexWithAliases(resolvedIndex, remoteClusterName) + } + } + } + } + + private def getRemoteClusterClient(remoteClusterService: RemoteClusterService, + remoteClusterName: ClusterName.Full) = { + Try(remoteClusterService.getRemoteClusterClient(remoteClusterName.value.value, scheduler)) + } + + private def resolveRemoteIndicesUsing(client: RemoteClusterClient) = { + import tech.beshu.ror.es.utils.ThreadContextOps._ + threadPool.getThreadContext.addXpackSecurityAuthenticationHeader(nodeName) + val promise = CancelablePromise[ResolveIndexAction.Response]() + client + .execute( + ResolveIndexAction.REMOTE_TYPE, + new ResolveIndexAction.Request(Array("*")), + new ActionListener[ResolveIndexAction.Response] { + override def onResponse(response: ResolveIndexAction.Response): Unit = promise.trySuccess(response) + override def onFailure(e: Exception): Unit = promise.tryFailure(e) + } + ) + Task.fromCancelablePromise(promise) + } + + private def toFullRemoteIndexWithAliases(resolvedIndex: ResolvedIndex, + remoteClusterName: ClusterName.Full) = { + IndexName.Full + .fromString(resolvedIndex.getName) + .map { index => + FullRemoteIndexWithAliases(remoteClusterName, index, indexAttributeFrom(resolvedIndex), aliasesFrom(resolvedIndex)) + } + } + + private def aliasesFrom(resolvedIndex: ResolvedIndex) = { + resolvedIndex + .getAliases.asSafeList + .flatMap(IndexName.Full.fromString) + .toSet + } + + private def aliasesPerIndexFrom(resolvedAliases: List[ResolvedAlias]) = { + lazy val mapMonoid: Monoid[Map[IndexName.Full, Set[IndexName.Full]]] = + Monoid[Map[IndexName.Full, Set[IndexName.Full]]] + resolvedAliases + .map { resolvedAlias => + resolvedAlias + .getIndices.asSafeList + .flatMap(IndexName.Full.fromString) + .map(index => (index, IndexName.Full.fromString(resolvedAlias.getName).toSet)) + .toMap + } + .foldLeft(Map.empty[IndexName.Full, Set[IndexName.Full]]) { + case (acc, aliasesPerIndex) => + mapMonoid.combine(acc, aliasesPerIndex) + } + } + + private def indexAttributeFrom(resolvedIndex: ResolvedIndex): IndexAttribute = { + resolvedIndex + .getAttributes.toSet + .find(_.toLowerCase == "CLOSED") match { + case Some(_) => IndexAttribute.Closed + case None => IndexAttribute.Opened + } + } + + private def snapshotsBy(repositoryName: RepositoryName) = { + repositoriesServiceSupplier.get() match { + case Some(repositoriesService) => + val repositoryData: RepositoryData = PlainActionFuture.get { fut: PlainActionFuture[RepositoryData] => + repositoriesService.getRepositoryData(RepositoryName.toString(repositoryName), fut) + } + repositoryData + .getSnapshotIds.asSafeIterable + .flatMap { sId => + SnapshotName + .from(sId.getName) + .flatMap { + case SnapshotName.Wildcard => None + case SnapshotName.All => None + case SnapshotName.Pattern(_) => None + case f: SnapshotName.Full => Some(f) + } + } + .toSet + case None => + logger.error("Cannot supply Snapshots Service. Please, report the issue!!!") + Set.empty[SnapshotName.Full] + } + } + + private def legacyTemplates(): Set[Template.LegacyTemplate] = { + val templates = clusterService.state.metadata().templates() + templates + .keySet().asScala + .flatMap { templateNameString => + val templateMetaData = templates.get(templateNameString) + for { + templateName <- NonEmptyString.unapply(templateNameString).map(TemplateName.apply) + indexPatterns <- UniqueNonEmptyList.fromIterable( + templateMetaData.patterns().asScala.flatMap(IndexPattern.fromString) + ) + aliases = templateMetaData.aliases().asSafeValues.flatMap(a => ClusterIndexName.fromString(a.alias())) + } yield Template.LegacyTemplate(templateName, indexPatterns, aliases) + } + .toSet + } + + private def indexTemplates(): Set[Template.IndexTemplate] = { + val templates = clusterService.state.metadata().templatesV2() + templates + .keySet().asScala + .flatMap { templateNameString => + val templateMetaData = templates.get(templateNameString) + for { + templateName <- NonEmptyString.unapply(templateNameString).map(TemplateName.apply) + indexPatterns <- UniqueNonEmptyList.fromIterable( + templateMetaData.indexPatterns().asScala.flatMap(IndexPattern.fromString) + ) + aliases = templateMetaData.template().asSafeSet + .flatMap(_.aliases().asSafeMap.values.flatMap(a => ClusterIndexName.fromString(a.alias())).toSet) + } yield Template.IndexTemplate(templateName, indexPatterns, aliases) + } + .toSet + } + + private def componentTemplates(): Set[Template.ComponentTemplate] = { + val templates = clusterService.state.metadata().componentTemplates() + templates + .keySet().asScala + .flatMap { templateNameString => + val templateMetaData = templates.get(templateNameString) + for { + templateName <- NonEmptyString.unapply(templateNameString).map(TemplateName.apply) + aliases = templateMetaData.template().aliases().asSafeMap.values.flatMap(a => ClusterIndexName.fromString(a.alias())).toSet + } yield Template.ComponentTemplate(templateName, aliases) + } + .toSet + } + + private def createSearchRequest(filter: Filter, + document: Document): SearchRequestBuilder = { + val wrappedQueryFromFilter = QueryBuilders.wrapperQuery(filter.value.value) + val composedQuery = QueryBuilders + .boolQuery() + .filter(QueryBuilders.constantScoreQuery(wrappedQueryFromFilter)) + .filter(QueryBuilders.idsQuery().addIds(document.documentId.value)) + + nodeClient + .prepareSearch(document.index.stringify) + .setQuery(composedQuery) + } + + private def extractAccessibilityFrom(searchResponse: SearchResponse) = { + if (searchResponse.getHits.getTotalHits.value == 0L) Inaccessible + else Accessible + } + + private def createMultiSearchRequest(definedFilter: Filter, + documents: NonEmptyList[Document]) = { + documents + .map(createSearchRequest(definedFilter, _)) + .foldLeft(nodeClient.prepareMultiSearch())(_ add _) + } + + private def blockAllDocsReturned(docsToVerify: NonEmptyList[Document]) = { + List.fill(docsToVerify.size)(Inaccessible) + } + + private def extractResultsFromSearchResponse(multiSearchResponse: MultiSearchResponse) = { + multiSearchResponse + .getResponses + .map(resolveAccessibilityBasedOnSearchResult) + .toList + } + + private def resolveAccessibilityBasedOnSearchResult(mSearchItem: MultiSearchResponse.Item): DocumentAccessibility = { + if (mSearchItem.isFailure) Inaccessible + else if (mSearchItem.getResponse.getHits.getTotalHits.value == 0L) Inaccessible + else Accessible + } + + private def zip(results: List[DocumentAccessibility], + documents: NonEmptyList[Document]) = { + documents.toList + .zip(results) + .toMap + } + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/services/RestClientAuditSinkService.scala b/es813x/src/main/scala/tech/beshu/ror/es/services/RestClientAuditSinkService.scala new file mode 100644 index 0000000000..2df40c60f0 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/services/RestClientAuditSinkService.scala @@ -0,0 +1,133 @@ +/* + * 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.services + +import cats.data.NonEmptyList +import io.lemonlabs.uri.Uri +import org.apache.http.HttpHost +import org.apache.http.auth.{AuthScope, Credentials, UsernamePasswordCredentials} +import org.apache.http.conn.ssl.NoopHostnameVerifier +import org.apache.http.impl.client.BasicCredentialsProvider +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.client.{Request, Response, ResponseListener, RestClient} +import tech.beshu.ror.accesscontrol.domain.AuditCluster +import tech.beshu.ror.es.AuditSinkService + +import java.security.cert.X509Certificate +import javax.net.ssl.{SSLContext, TrustManager, X509TrustManager} +import scala.collection.parallel.CollectionConverters._ + +class RestClientAuditSinkService private(clients: NonEmptyList[RestClient]) + extends AuditSinkService + with Logging { + + override def submit(indexName: String, documentId: String, jsonRecord: String): Unit = { + clients.toList.par.foreach { client => + client + .performRequestAsync( + createRequest(indexName, documentId, jsonRecord), + createResponseListener(indexName, documentId) + ) + } + } + + override def close(): Unit = { + clients.toList.par.foreach(_.close()) + } + + private def createRequest(indexName: String, documentId: String, jsonBody: String) = { + val request = new Request("PUT", s"/$indexName/_doc/$documentId") + request.setJsonEntity(jsonBody) + request + } + + private def createResponseListener(indexName: String, + documentId: String) = + new ResponseListener() { + override def onSuccess(response: Response): Unit = { + response.getStatusLine.getStatusCode / 100 match { + case 2 => // 2xx + case _ => + logger.error(s"Cannot submit audit event [index: $indexName, doc: $documentId] - response code: ${response.getStatusLine.getStatusCode}") + } + } + + override def onFailure(ex: Exception): Unit = { + logger.error(s"Cannot submit audit event [index: $indexName, doc: $documentId]", ex) + } + } +} + +object RestClientAuditSinkService { + + def create(remoteCluster: AuditCluster.RemoteAuditCluster): RestClientAuditSinkService = { + val clients = remoteCluster.uris.map(createRestClient) + new RestClientAuditSinkService(clients) + } + + private def createRestClient(uri: Uri) = { + val host = new HttpHost( + uri.toUrl.hostOption.map(_.value).getOrElse("localhost"), + uri.toUrl.port.getOrElse(9200), + uri.schemeOption.getOrElse("http") + ) + val credentials: Option[Credentials] = uri.toUrl.user.map { user => + new UsernamePasswordCredentials(user, uri.toUrl.password.getOrElse("")) + } + + RestClient + .builder(host) + .setHttpClientConfigCallback( + (httpClientBuilder: HttpAsyncClientBuilder) => { + val configurations = configureCredentials(credentials) andThen configureSsl() + configurations apply httpClientBuilder + } + ) + .build() + } + + private def configureCredentials(credentials: Option[Credentials]): HttpAsyncClientBuilder => HttpAsyncClientBuilder = (httpClientBuilder: HttpAsyncClientBuilder) => { + credentials match { + case Some(c) => + httpClientBuilder + .disableAuthCaching() + .setDefaultCredentialsProvider { + val credentialsProvider = new BasicCredentialsProvider + credentialsProvider.setCredentials(AuthScope.ANY, c) + credentialsProvider + } + case None => + httpClientBuilder + } + } + + private def configureSsl(): HttpAsyncClientBuilder => HttpAsyncClientBuilder = (httpClientBuilder: HttpAsyncClientBuilder) => { + val trustAllCerts = createTrustAllManager() + val sslContext = SSLContext.getInstance("TLSv1.2") + sslContext.init(null, Array(trustAllCerts), null) + httpClientBuilder + .setSSLContext(sslContext) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) + } + + private def createTrustAllManager(): TrustManager = new X509TrustManager() { + override def checkClientTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = () + override def checkServerTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = () + override def getAcceptedIssuers: Array[X509Certificate] = null + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/ssl/SSLNetty4HttpServerTransport.scala b/es813x/src/main/scala/tech/beshu/ror/es/ssl/SSLNetty4HttpServerTransport.scala new file mode 100644 index 0000000000..ce17639c06 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/ssl/SSLNetty4HttpServerTransport.scala @@ -0,0 +1,65 @@ +/* + * 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.ssl + +import io.netty.channel.Channel +import io.netty.handler.ssl.NotSslRecordException +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.common.network.NetworkService +import org.elasticsearch.common.settings.{ClusterSettings, Settings} +import org.elasticsearch.http.netty4.Netty4HttpServerTransport +import org.elasticsearch.http.{HttpChannel, HttpServerTransport} +import org.elasticsearch.telemetry.tracing.Tracer +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.netty4.{SharedGroupFactory, TLSConfig} +import org.elasticsearch.xcontent.NamedXContentRegistry +import tech.beshu.ror.configuration.SslConfiguration.ExternalSslConfiguration +import tech.beshu.ror.utils.SSLCertHelper + +class SSLNetty4HttpServerTransport(settings: Settings, + networkService: NetworkService, + threadPool: ThreadPool, + xContentRegistry: NamedXContentRegistry, + dispatcher: HttpServerTransport.Dispatcher, + ssl: ExternalSslConfiguration, + clusterSettings: ClusterSettings, + sharedGroupFactory: SharedGroupFactory, + tracer: Tracer, + fipsCompliant: Boolean) + extends Netty4HttpServerTransport(settings, networkService, threadPool, xContentRegistry, dispatcher, clusterSettings, sharedGroupFactory, tracer, TLSConfig.noTLS(), null, null) + with Logging { + + private val serverSslContext = SSLCertHelper.prepareServerSSLContext(ssl, fipsCompliant, ssl.clientAuthenticationEnabled) + + override def configureServerChannelHandler = new SSLHandler(this) + + override def onException(channel: HttpChannel, cause: Exception): Unit = { + if (!this.lifecycle.started) return + else if (cause.getCause.isInstanceOf[NotSslRecordException]) logger.warn(cause.getMessage + " connecting from: " + channel.getRemoteAddress) + else super.onException(channel, cause) + channel.close() + } + + final class SSLHandler(transport: Netty4HttpServerTransport) + extends Netty4HttpServerTransport.HttpChannelHandler(transport, handlingSettings, TLSConfig.noTLS(), null, null) { + + override def initChannel(ch: Channel): Unit = { + super.initChannel(ch) + ch.pipeline().addFirst("ssl_netty4_handler", serverSslContext.newHandler(ch.alloc())) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/ssl/SSLNetty4InternodeServerTransport.scala b/es813x/src/main/scala/tech/beshu/ror/es/ssl/SSLNetty4InternodeServerTransport.scala new file mode 100644 index 0000000000..4cc76adca0 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/ssl/SSLNetty4InternodeServerTransport.scala @@ -0,0 +1,96 @@ +/* + * 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.ssl + +import io.netty.channel._ +import io.netty.handler.ssl._ +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.TransportVersion +import org.elasticsearch.cluster.node.DiscoveryNode +import org.elasticsearch.common.io.stream.NamedWriteableRegistry +import org.elasticsearch.common.network.NetworkService +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.common.util.PageCacheRecycler +import org.elasticsearch.indices.breaker.CircuitBreakerService +import org.elasticsearch.threadpool.ThreadPool +import org.elasticsearch.transport.ConnectionProfile +import org.elasticsearch.transport.netty4.{Netty4Transport, SharedGroupFactory} +import tech.beshu.ror.configuration.SslConfiguration.InternodeSslConfiguration +import tech.beshu.ror.utils.SSLCertHelper +import tech.beshu.ror.utils.SSLCertHelper.HostAndPort + +import java.net.{InetSocketAddress, SocketAddress} +import javax.net.ssl.SNIHostName + +class SSLNetty4InternodeServerTransport(settings: Settings, + threadPool: ThreadPool, + pageCacheRecycler: PageCacheRecycler, + circuitBreakerService: CircuitBreakerService, + namedWriteableRegistry: NamedWriteableRegistry, + networkService: NetworkService, + ssl: InternodeSslConfiguration, + sharedGroupFactory: SharedGroupFactory, + fipsCompliant: Boolean) + extends Netty4Transport(settings, TransportVersion.current(), threadPool, networkService, pageCacheRecycler, namedWriteableRegistry, circuitBreakerService, sharedGroupFactory) + with Logging { + + private val clientSslContext = SSLCertHelper.prepareClientSSLContext(ssl, fipsCompliant, ssl.certificateVerificationEnabled) + private val serverSslContext = SSLCertHelper.prepareServerSSLContext(ssl, fipsCompliant, clientAuthenticationEnabled = false) + + override def getClientChannelInitializer(node: DiscoveryNode, + connectionProfile: ConnectionProfile): ChannelHandler = new ClientChannelInitializer { + override def initChannel(ch: Channel): Unit = { + super.initChannel(ch) + + ch.pipeline().addFirst(new ChannelOutboundHandlerAdapter { + override def connect(ctx: ChannelHandlerContext, + remoteAddress: SocketAddress, + localAddress: SocketAddress, + promise: ChannelPromise): Unit = { + val inet = remoteAddress.asInstanceOf[InetSocketAddress] + val sslEngine = SSLCertHelper.prepareSSLEngine( + sslContext = clientSslContext, + hostAndPort = HostAndPort(inet.getHostString, inet.getPort), + channelHandlerContext = ctx, + serverName = Option(node.getAttributes.get("server_name")).map(new SNIHostName(_)), + enableHostnameVerification = ssl.hostnameVerificationEnabled, + fipsCompliant = fipsCompliant + ) + ctx.pipeline().replace(this, "internode_ssl_client", new SslHandler(sslEngine)) + super.connect(ctx, remoteAddress, localAddress, promise) + } + }) + } + + override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { + if (cause.isInstanceOf[NotSslRecordException] || (cause.getCause != null && cause.getCause.isInstanceOf[NotSslRecordException])) { + logger.error("Receiving non-SSL connections from: (" + ctx.channel.remoteAddress + "). Will disconnect") + ctx.channel.close + } else { + super.exceptionCaught(ctx, cause) + } + } + } + + override def getServerChannelInitializer(name: String): ChannelHandler = new ServerChannelInitializer(name) { + + override def initChannel(ch: Channel): Unit = { + super.initChannel(ch) + ch.pipeline().addFirst("ror_internode_ssl_handler", serverSslContext.newHandler(ch.alloc())) + } + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/CallActionRequestAndHandleResponse.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/CallActionRequestAndHandleResponse.scala new file mode 100644 index 0000000000..46935dc885 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/CallActionRequestAndHandleResponse.scala @@ -0,0 +1,59 @@ +/* + * 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.utils + +import monix.eval.Task +import monix.execution.atomic.Atomic +import org.elasticsearch.action.{ActionListener, ActionRequest, ActionRequestBuilder, ActionResponse} + +import scala.concurrent.Promise +import scala.language.implicitConversions + +class CallActionRequestAndHandleResponse[REQUEST <: ActionRequest, RESPONSE <: ActionResponse] private(builder: ActionRequestBuilder[REQUEST, RESPONSE]) { + + def call[R](f: RESPONSE => R): Task[R] = { + val listener = new GenericResponseListener() + builder.execute(listener) + listener.result(f) + } + + private final class GenericResponseListener extends ActionListener[RESPONSE] { + + private val promise = Promise[RESPONSE]() + private val finalizer = Atomic(Task.unit) + + def result[T](f: RESPONSE => T): Task[T] = Task + .fromFuture(promise.future) + .map(f) + .guarantee(finalizer.getAndSet(Task.unit)) + + override def onResponse(response: RESPONSE): Unit = { + response.incRef() + finalizer.set(Task.delay(response.decRef())) + promise.success(response) + } + + override def onFailure(exception: Exception): Unit = { + promise.failure(exception) + } + } +} + +object CallActionRequestAndHandleResponse { + implicit def toOps[REQUEST <: ActionRequest, RESPONSE <: ActionResponse](builder: ActionRequestBuilder[REQUEST, RESPONSE]): CallActionRequestAndHandleResponse[REQUEST, RESPONSE] = + new CallActionRequestAndHandleResponse(builder) +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/ChannelInterceptingRestHandlerDecorator.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/ChannelInterceptingRestHandlerDecorator.scala new file mode 100644 index 0000000000..122fcfd39a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/ChannelInterceptingRestHandlerDecorator.scala @@ -0,0 +1,97 @@ +/* + * 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.utils + +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.rest.action.admin.indices.RestUpgradeActionDeprecated +import org.elasticsearch.rest.action.cat.RestCatAction +import org.elasticsearch.rest.{RestChannel, RestHandler, RestRequest, Scope} +import org.joor.Reflect.on +import tech.beshu.ror.es.RorRestChannel +import tech.beshu.ror.es.actions.wrappers._cat.rest.RorWrappedRestCatAction +import tech.beshu.ror.es.actions.wrappers._upgrade.rest.RorWrappedRestUpgradeAction +import tech.beshu.ror.es.utils.ThreadContextOps.createThreadContextOps +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged + +import java.util +import scala.util.Try + +class ChannelInterceptingRestHandlerDecorator private(val underlying: RestHandler) + extends RestHandler { + + private val wrapped = doPrivileged { + wrapSomeActions(underlying) + } + + override def handleRequest(request: RestRequest, channel: RestChannel, client: NodeClient): Unit = { + val rorRestChannel = new RorRestChannel(channel) + ThreadRepo.setRestChannel(rorRestChannel) + addRorUserAuthenticationHeaderForInCaseOfSecurityRequest(request, client) + wrapped.handleRequest(request, rorRestChannel, client) + } + + override def canTripCircuitBreaker: Boolean = underlying.canTripCircuitBreaker + + override def supportsContentStream(): Boolean = underlying.supportsContentStream() + + override def getConcreteRestHandler: RestHandler = underlying.getConcreteRestHandler + + override def getServerlessScope: Scope = underlying.getServerlessScope + + override def allowsUnsafeBuffers(): Boolean = underlying.allowsUnsafeBuffers() + + override def routes(): util.List[RestHandler.Route] = underlying.routes() + + override def allowSystemIndexAccessByDefault(): Boolean = underlying.allowSystemIndexAccessByDefault() + + override def mediaTypesValid(request: RestRequest): Boolean = underlying.mediaTypesValid(request) + + private def wrapSomeActions(ofHandler: RestHandler) = { + unwrapWithSecurityRestFilterIfNeeded(ofHandler) match { + case action: RestCatAction => new RorWrappedRestCatAction(action) + case action: RestUpgradeActionDeprecated => new RorWrappedRestUpgradeAction(action) + case action => action + } + } + + private def unwrapWithSecurityRestFilterIfNeeded(restHandler: RestHandler) = { + restHandler match { + case action if action.getClass.getName.contains("SecurityRestFilter") => + Try(on(restHandler).get[RestHandler]("delegate")) + .getOrElse(action) + case _ => + restHandler + } + } + + private def addRorUserAuthenticationHeaderForInCaseOfSecurityRequest(request: RestRequest, + client: NodeClient): Unit = { + if (request.path().contains("/_security") || request.path().contains("/_xpack/security")) { + client + .threadPool().getThreadContext + .addRorUserAuthenticationHeader(client.getLocalNodeId) + } + } + +} + +object ChannelInterceptingRestHandlerDecorator { + def create(restHandler: RestHandler): ChannelInterceptingRestHandlerDecorator = restHandler match { + case alreadyDecoratedHandler: ChannelInterceptingRestHandlerDecorator => alreadyDecoratedHandler + case handler => new ChannelInterceptingRestHandlerDecorator(handler) + } +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/EsCollectionsScalaUtils.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/EsCollectionsScalaUtils.scala new file mode 100644 index 0000000000..ed6b472541 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/EsCollectionsScalaUtils.scala @@ -0,0 +1,56 @@ +/* + * 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.utils + +import org.elasticsearch.common.collect.ImmutableOpenMap + +import scala.jdk.CollectionConverters._ + +object EsCollectionsScalaUtils { + + implicit class ImmutableOpenMapOps[K, V](val value: ImmutableOpenMap[K, V]) extends AnyVal { + + def asSafeKeys: Set[K] = Option(value).map(_.keySet().asScala.toSet).getOrElse(Set.empty) + + def asSafeValues: Set[V] = Option(value).map(_.values().asScala.toSet).getOrElse(Set.empty) + + def asSafeEntriesList: List[(K, V)] = + Option(value) match { + case Some(map) => + map + .keySet().asScala + .map { key => + (key, map.get(key)) + } + .toList + case None => + List.empty + } + } + + object ImmutableOpenMapOps { + def from[K, V](map: Map[K, V]): ImmutableOpenMap[K, V] = { + ImmutableOpenMap + .builder[K, V]() + .putAllFromMap(map.asJava) + .build() + } + + def empty[K, V]: ImmutableOpenMap[K, V] = + from(Map.empty) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/EsPatchVerifier.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/EsPatchVerifier.scala new file mode 100644 index 0000000000..5ab1e35742 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/EsPatchVerifier.scala @@ -0,0 +1,43 @@ +/* + * 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.utils + +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.common.settings.Settings +import tech.beshu.ror.tools.core.patches.PatchingVerifier +import tech.beshu.ror.tools.core.patches.PatchingVerifier.Error.{CannotVerifyIfPatched, EsNotPatched} +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged + +object EsPatchVerifier extends Logging { + + def verify(settings: Settings): Unit = doPrivileged { + pathHomeFrom(settings).flatMap(PatchingVerifier.verify) match { + case Right(_) => + case Left(e@EsNotPatched(_)) => + throw new IllegalStateException(e.message) + case Left(e@CannotVerifyIfPatched(_)) => + logger.warn(e.message) + } + } + + private def pathHomeFrom(settings: Settings) = + Option(settings.get("path.home")) match { + case Some(esPath) => Right(esPath) + case None => Left(CannotVerifyIfPatched("No 'path.home' setting.")) + } +} + diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/IndexModuleOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/IndexModuleOps.scala new file mode 100644 index 0000000000..0af5ae23fc --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/IndexModuleOps.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.utils + +import org.apache.lucene.index.DirectoryReader +import org.apache.lucene.util.SetOnce.AlreadySetException +import org.apache.lucene.util.{SetOnce => LuceneSetOnce} +import org.elasticsearch.core.CheckedFunction +import org.elasticsearch.index.{IndexModule, IndexService} +import org.joor.Reflect.on +import tech.beshu.ror.es.utils.IndexModuleOps.ReaderWrapper +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged + +import java.io.IOException +import java.util.function.{Function => JFunction} +import scala.annotation.tailrec +import scala.language.implicitConversions +import scala.util.{Failure, Success, Try} + +class IndexModuleOps(indexModule: IndexModule) { + + def overwrite(readerWrapper: ReaderWrapper): Unit = { + doPrivileged { + doOverwrite(readerWrapper) + } + } + + @tailrec + private def doOverwrite(readerWrapper: ReaderWrapper, triesLeft: Int = 1): Unit = { + Try { + indexModule.setReaderWrapper(readerWrapper) + } match { + case Success(()) => () + case Failure(_: AlreadySetException) if triesLeft > 0 => + on(indexModule).set("indexReaderWrapper", new LuceneSetOnce[ReaderWrapper]()) + doOverwrite(readerWrapper, triesLeft - 1) + case Failure(ex) => + throw ex + } + } +} + +object IndexModuleOps { + type ReaderWrapper = JFunction[IndexService, CheckedFunction[DirectoryReader, DirectoryReader, IOException]] + + implicit def toOps(indexModule: IndexModule): IndexModuleOps = new IndexModuleOps(indexModule) + +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/RemoteClusterServiceSupplier.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/RemoteClusterServiceSupplier.scala new file mode 100644 index 0000000000..bfe553ab1d --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/RemoteClusterServiceSupplier.scala @@ -0,0 +1,58 @@ +/* + * 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.utils + +import org.apache.logging.log4j.scala.Logging +import org.elasticsearch.repositories.{RepositoriesService, VerifyNodeRepositoryAction} +import org.elasticsearch.transport.{RemoteClusterService, TransportService} +import org.joor.Reflect.on +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged + +import java.util.function.Supplier +import scala.util.{Failure, Success, Try} + +class RemoteClusterServiceSupplier(repositoriesServiceSupplier: Supplier[RepositoriesService]) + extends Supplier[Option[RemoteClusterService]] with Logging { + + override def get(): Option[RemoteClusterService] = { + for { + repositoriesService <- Option(repositoriesServiceSupplier.get()) + remoteClusterService <- extractTransportServiceFrom(repositoriesService) match { + case Success(transportService) => + Option(transportService.getRemoteClusterService) + case Failure(ex) => + logger.error("Cannot extract RemoteClusterService from RepositoriesService", ex) + None + } + } yield remoteClusterService + } + + private def extractTransportServiceFrom(repositoriesService: RepositoriesService) = doPrivileged { + for { + action <- getVerifyNodeRepositoryActionFrom(repositoriesService) + transportService <- getTransportServiceFrom(action) + } yield transportService + } + + private def getVerifyNodeRepositoryActionFrom(repositoriesService: RepositoriesService) = Try { + on(repositoriesService).get[VerifyNodeRepositoryAction]("verifyAction") + } + + private def getTransportServiceFrom(action: VerifyNodeRepositoryAction) = Try { + on(action).get[TransportService]("transportService") + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/RestControllerOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/RestControllerOps.scala new file mode 100644 index 0000000000..4824027509 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/RestControllerOps.scala @@ -0,0 +1,116 @@ +/* + * 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.utils + +import org.elasticsearch.action.ActionListener +import org.elasticsearch.client.internal.node.NodeClient +import org.elasticsearch.common.path.PathTrie +import org.elasticsearch.core.RestApiVersion +import org.elasticsearch.rest.{RestChannel, RestController, RestHandler, RestInterceptor, RestRequest} +import org.joor.Reflect.on +import tech.beshu.ror.utils.AccessControllerHelper.doPrivileged +import tech.beshu.ror.utils.ScalaOps._ + +import java.lang +import scala.jdk.CollectionConverters._ +import scala.language.implicitConversions +import scala.util.{Failure, Success, Try} + +class RestControllerOps(val restController: RestController) { + + def decorateRestHandlersWith(restHandlerDecorator: RestHandler => RestHandler): Unit = doPrivileged { + val nodeClient = on(restController).get[NodeClient]("client") + on(restController).set("interceptor", new ChannelInterceptingRestInterceptor(nodeClient)) + + val handlers = on(restController).get[PathTrie[Any]]("handlers") + val updatedHandlers = new PathTreeOps(handlers).update(restHandlerDecorator) + on(restController).set("handlers", updatedHandlers) + } + + private final class PathTreeOps(pathTrie: PathTrie[Any]) { + + def update(restHandlerDecorator: RestHandler => RestHandler): PathTrie[Any] = { + val root = on(pathTrie).get[pathTrie.TrieNode]("root") + update(root, restHandlerDecorator) + pathTrie + } + + private def update(trieNode: pathTrie.TrieNode, + restHandlerDecorator: RestHandler => RestHandler): Unit = { + Option(on(trieNode).get[Any]("value")).foreach { value => + MethodHandlersWrapper.updateWithWrapper(value, restHandlerDecorator) + } + Option(on(pathTrie).get[Any]("rootValue")).foreach { value => + MethodHandlersWrapper.updateWithWrapper(value, restHandlerDecorator) + } + val children = on(trieNode).get[java.util.Map[String, pathTrie.TrieNode]]("children").asSafeMap + children.values.foreach { trieNode => + update(trieNode, restHandlerDecorator) + } + } + } + + private object MethodHandlersWrapper { + def updateWithWrapper(value: Any, restHandlerDecorator: RestHandler => RestHandler): Any = { + val methodHandlers = on(value).get[java.util.Map[RestRequest.Method, java.util.Map[RestApiVersion, RestHandler]]]("methodHandlers").asScala.toMap + val newMethodHandlers = methodHandlers + .map { case (key, handlersMap) => + val newHandlersMap = handlersMap + .asSafeMap + .map { case (apiVersion, handler) => + (apiVersion, restHandlerDecorator(removeSecurityHandler(handler))) + } + .asJava + (key, newHandlersMap) + } + .asJava + on(value).set("methodHandlers", newMethodHandlers) + value + } + + private def removeSecurityHandler(handler: RestHandler) = { + val toProcessing = handler match { + case h: ChannelInterceptingRestHandlerDecorator => h.underlying + case h => h + } + Try(on(toProcessing).get[RestHandler]("restHandler")) match { + case Success(underlyingHandler) => + underlyingHandler + case Failure(_) => + toProcessing + } + } + } + + private class ChannelInterceptingRestInterceptor(nodeClient: NodeClient) extends RestInterceptor { + + override def intercept(request: RestRequest, + channel: RestChannel, + targetHandler: RestHandler, + listener: ActionListener[lang.Boolean]): Unit = { + ChannelInterceptingRestHandlerDecorator + .create(targetHandler) + .handleRequest(request, channel, nodeClient) + } + } + +} + +object RestControllerOps { + + implicit def toOps(restController: RestController): RestControllerOps = new RestControllerOps(restController) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/RestToXContentWithStatusListener.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/RestToXContentWithStatusListener.scala new file mode 100644 index 0000000000..d8a893ccfa --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/RestToXContentWithStatusListener.scala @@ -0,0 +1,33 @@ +/* + * 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.utils + +import org.elasticsearch.action.ActionResponse +import org.elasticsearch.rest.action.RestToXContentListener +import org.elasticsearch.rest.{RestChannel, RestStatus} +import org.elasticsearch.xcontent.ToXContentObject + +class RestToXContentWithStatusListener[RESPONSE <: ActionResponse with StatusToXContentObject](channel: RestChannel) + extends RestToXContentListener[RESPONSE]( + channel, + (response: RESPONSE) => response.status + ) + +trait StatusToXContentObject extends ToXContentObject { + + def status: RestStatus +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/SqlRequestHelper.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/SqlRequestHelper.scala new file mode 100644 index 0000000000..9af9c9a763 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/SqlRequestHelper.scala @@ -0,0 +1,347 @@ +/* + * 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.utils + +import java.lang.reflect.Modifier +import java.time.ZoneId +import java.util.regex.Pattern +import java.util.{List => JList} + +import org.elasticsearch.action.{ActionResponse, CompositeIndicesRequest} +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity +import tech.beshu.ror.accesscontrol.domain.FieldLevelSecurity.FieldsRestrictions +import tech.beshu.ror.es.handler.response.FieldsFiltering +import tech.beshu.ror.es.handler.response.FieldsFiltering.NonMetadataDocumentFields +import tech.beshu.ror.es.utils.ExtractedIndices.SqlIndices +import tech.beshu.ror.es.utils.ExtractedIndices.SqlIndices.SqlTableRelated.IndexSqlTable +import tech.beshu.ror.es.utils.ExtractedIndices.SqlIndices.{SqlNotTableRelated, SqlTableRelated} +import tech.beshu.ror.utils.ReflecUtils +import tech.beshu.ror.utils.ScalaOps._ + +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +sealed trait ExtractedIndices { + def indices: Set[String] +} +object ExtractedIndices { + case object NoIndices extends ExtractedIndices { + override def indices: Set[String] = Set.empty + } + final case class RegularIndices(override val indices: Set[String]) extends ExtractedIndices + sealed trait SqlIndices extends ExtractedIndices { + def indices: Set[String] + } + object SqlIndices { + final case class SqlTableRelated(tables: List[IndexSqlTable]) extends SqlIndices { + override lazy val indices: Set[String] = tables.flatMap(_.indices).toSet + } + object SqlTableRelated { + final case class IndexSqlTable(tableStringInQuery: String, indices: Set[String]) + } + case object SqlNotTableRelated extends SqlIndices { + override def indices: Set[String] = Set.empty + } + } +} + +object SqlRequestHelper { + + sealed trait ModificationError + object ModificationError { + final case class UnexpectedException(ex: Throwable) extends ModificationError + } + + def modifyIndicesOf(request: CompositeIndicesRequest, + extractedIndices: SqlIndices, + finalIndices: Set[String]): Either[ModificationError, CompositeIndicesRequest] = { + val result = Try { + extractedIndices match { + case s: SqlTableRelated => + setQuery(request, newQueryFrom(getQuery(request), s, finalIndices)) + case SqlNotTableRelated => + request + } + } + result.toEither.left.map(ModificationError.UnexpectedException.apply) + } + + def modifyResponseAccordingToFieldLevelSecurity(response: ActionResponse, + fieldLevelSecurity: FieldLevelSecurity): Either[ModificationError, ActionResponse] = { + val result = Try { + implicit val classLoader: ClassLoader = response.getClass.getClassLoader + new SqlQueryResponse(response).modifyByApplyingRestrictions(fieldLevelSecurity.restrictions) + response + } + result.toEither.left.map(ModificationError.UnexpectedException.apply) + } + + sealed trait IndicesError + object IndicesError { + final case class UnexpectedException(ex: Throwable) extends IndicesError + case object ParsingException extends IndicesError + } + + def indicesFrom(request: CompositeIndicesRequest): Either[IndicesError, SqlIndices] = { + val result = Try { + val query = getQuery(request) + val params = ReflecUtils.invokeMethodCached(request, request.getClass, "params") + + implicit val classLoader: ClassLoader = request.getClass.getClassLoader + val statement = Try(new SqlParser().createStatement(query, params)) + statement match { + case Success(statement: SimpleStatement) => Right(statement.indices) + case Success(command: Command) => Right(command.indices) + case Failure(_) => Left(IndicesError.ParsingException: IndicesError) + } + } + result match { + case Success(value) => value + case Failure(exception) => Left(IndicesError.UnexpectedException(exception)) + } + } + + private def getQuery(request: CompositeIndicesRequest): String = { + ReflecUtils.invokeMethodCached(request, request.getClass, "query").asInstanceOf[String] + } + + private def setQuery(request: CompositeIndicesRequest, newQuery: String): CompositeIndicesRequest = { + ReflecUtils + .getMethodOf(request.getClass, Modifier.PUBLIC, "query", 1) + .invoke(request, newQuery) + request + } + + private def newQueryFrom(oldQuery: String, extractedIndices: SqlIndices.SqlTableRelated, finalIndices: Set[String]) = { + extractedIndices.tables match { + case Nil => + s"""$oldQuery "${finalIndices.mkString(",")}"""" + case tables => + tables.foldLeft(oldQuery) { + case (currentQuery, table) => + val (beforeFrom, afterFrom) = currentQuery.splitBy("FROM") + afterFrom match { + case None => + replaceTableNameInQueryPart(currentQuery, table.tableStringInQuery, finalIndices) + case Some(tablesPart) => + s"${beforeFrom}FROM ${replaceTableNameInQueryPart(tablesPart, table.tableStringInQuery, finalIndices)}" + } + } + } + } + + private def replaceTableNameInQueryPart(currentQuery: String, originTable: String, finalIndices: Set[String]) = { + currentQuery.replaceAll(Pattern.quote(originTable), finalIndices.mkString(",")) + } +} + +final class SqlParser(implicit classLoader: ClassLoader) { + + private val aClass = classLoader.loadClass("org.elasticsearch.xpack.sql.parser.SqlParser") + private val underlyingObject = aClass.getConstructor().newInstance() + + def createStatement(query: String, params: AnyRef): Statement = { + val statement = ReflecUtils + .getMethodOf(aClass, Modifier.PUBLIC, "createStatement", 3) + .invoke(underlyingObject, query, params, ZoneId.systemDefault()) + if (Command.isClassOf(statement)) new Command(statement) + else new SimpleStatement(statement) + } + +} + +sealed trait Statement { + protected def splitToIndicesPatterns(value: String): Set[String] = { + value.split(',').asSafeSet.filter(_.nonEmpty) + } +} + +final class SimpleStatement(val underlyingObject: AnyRef) + (implicit classLoader: ClassLoader) + extends Statement { + + lazy val indices: SqlIndices = { + val tableInfoList = tableInfosFrom { + doPreAnalyze(newPreAnalyzer, underlyingObject) + } + SqlIndices.SqlTableRelated { + tableInfoList + .map(tableIdentifierFrom) + .map(indicesStringFrom) + .map { tableString => + IndexSqlTable(tableString, splitToIndicesPatterns(tableString)) + } + } + } + + private def newPreAnalyzer(implicit classLoader: ClassLoader) = { + val preAnalyzerConstructor = preAnalyzerClass.getConstructor() + preAnalyzerConstructor.newInstance() + } + + private def doPreAnalyze(preAnalyzer: Any, statement: AnyRef) + (implicit classLoader: ClassLoader) = { + ReflecUtils + .getMethodOf(preAnalyzerClass, Modifier.PUBLIC, "preAnalyze", 1) + .invoke(preAnalyzer, statement) + } + + private def tableInfosFrom(preAnalysis: Any) + (implicit classLoader: ClassLoader) = { + ReflecUtils + .getFieldOf(preAnalysisClass, Modifier.PUBLIC, "indices") + .get(preAnalysis) + .asInstanceOf[java.util.List[AnyRef]] + .asScala.toList + } + + private def tableIdentifierFrom(tableInfo: Any) + (implicit classLoader: ClassLoader) = { + ReflecUtils + .getMethodOf(tableInfoClass, Modifier.PUBLIC, "id", 0) + .invoke(tableInfo) + } + + private def indicesStringFrom(tableIdentifier: Any) + (implicit classLoader: ClassLoader) = { + ReflecUtils + .getMethodOf(tableIdentifierClass, Modifier.PUBLIC, "index", 0) + .invoke(tableIdentifier) + .asInstanceOf[String] + } + + private def preAnalyzerClass(implicit classLoader: ClassLoader) = + classLoader.loadClass("org.elasticsearch.xpack.ql.analyzer.PreAnalyzer") + + private def preAnalysisClass(implicit classLoader: ClassLoader) = + classLoader.loadClass("org.elasticsearch.xpack.ql.analyzer.PreAnalyzer$PreAnalysis") + + private def tableInfoClass(implicit classLoader: ClassLoader) = + classLoader.loadClass("org.elasticsearch.xpack.ql.analyzer.TableInfo") + + private def tableIdentifierClass(implicit classLoader: ClassLoader) = + classLoader.loadClass("org.elasticsearch.xpack.ql.plan.TableIdentifier") +} + +final class Command(val underlyingObject: Any) + extends Statement { + + lazy val indices: SqlIndices = { + Try { + getIndicesString + .orElse(getIndexPatternsString) + .map { indicesString => + SqlTableRelated(IndexSqlTable(indicesString, splitToIndicesPatterns(indicesString)) :: Nil) + } + .getOrElse(SqlTableRelated(Nil)) + } getOrElse { + SqlNotTableRelated + } + } + + private def getIndicesString = Option { + ReflecUtils + .getMethodOf(underlyingObject.getClass, Modifier.PUBLIC, "index", 0) + .invoke(underlyingObject) + .asInstanceOf[String] + } + + private def getIndexPatternsString = { + for { + pattern <- Option(ReflecUtils + .getMethodOf(underlyingObject.getClass, Modifier.PUBLIC, "pattern", 0) + .invoke(underlyingObject)) + index <- Option(ReflecUtils + .getMethodOf(pattern.getClass, Modifier.PUBLIC, "asIndexNameWildcard", 0) + .invoke(pattern)) + } yield index.asInstanceOf[String] + } +} +object Command { + def isClassOf(obj: Any)(implicit classLoader: ClassLoader): Boolean = + commandClass.isAssignableFrom(obj.getClass) + + private def commandClass(implicit classLoader: ClassLoader): Class[_] = + classLoader.loadClass("org.elasticsearch.xpack.sql.plan.logical.command.Command") +} + +final class SqlQueryResponse(val underlyingObject: Any) + (implicit classLoader: ClassLoader) { + + def modifyByApplyingRestrictions(restrictions: FieldsRestrictions): Unit = { + val columnsAndValues = getColumns.zip(getRows.transpose) + val columnsMap = columnsAndValues.map { case (column, values) => (column.name, (column, values)) }.toMap + + val filteredColumnsAndValues = FieldsFiltering + .filterNonMetadataDocumentFields(NonMetadataDocumentFields(columnsMap), restrictions) + .value.values + + val filteredColumns = filteredColumnsAndValues.map(_._1).toList + val filteredRows = filteredColumnsAndValues.map(_._2).toList.transpose + + modifyColumns(filteredColumns) + modifyRows(filteredRows) + } + + private def getColumns: List[ColumnInfo] = { + ReflecUtils + .getMethodOf(sqlQueryResponseClass, Modifier.PUBLIC, "columns", 0) + .invoke(underlyingObject) + .asInstanceOf[JList[AnyRef]] + .asSafeList + .map(new ColumnInfo(_)) + } + + private def getRows: List[List[Value]] = { + ReflecUtils + .getMethodOf(sqlQueryResponseClass, Modifier.PUBLIC, "rows", 0) + .invoke(underlyingObject) + .asInstanceOf[JList[JList[AnyRef]]] + .asSafeList.map(_.asSafeList.map(new Value(_))) + } + + private def modifyColumns(columns: List[ColumnInfo]): Unit = { + ReflecUtils + .getMethodOf(sqlQueryResponseClass, Modifier.PUBLIC, "columns", 1) + .invoke(underlyingObject, columns.map(_.underlyingObject).asJava) + } + + private def modifyRows(rows: List[List[Value]]): Unit = { + ReflecUtils + .getMethodOf(sqlQueryResponseClass, Modifier.PUBLIC, "rows", 1) + .invoke(underlyingObject, rows.map(_.map(_.underlyingObject).asJava).asJava) + } + + private def sqlQueryResponseClass(implicit classLoader: ClassLoader) = + classLoader.loadClass("org.elasticsearch.xpack.sql.action.SqlQueryResponse") +} + +final class ColumnInfo(val underlyingObject: Any) + (implicit classLoader: ClassLoader) { + + val name: String = { + ReflecUtils + .getMethodOf(columnInfoClass, Modifier.PUBLIC, "name", 0) + .invoke(underlyingObject) + .asInstanceOf[String] + } + + private def columnInfoClass(implicit classLoader: ClassLoader) = + classLoader.loadClass("org.elasticsearch.xpack.sql.proto.ColumnInfo") +} + +final class Value(val underlyingObject: Any) \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/ThreadContextOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/ThreadContextOps.scala new file mode 100644 index 0000000000..b06ccc3290 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/ThreadContextOps.scala @@ -0,0 +1,59 @@ +/* + * 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.utils + +import org.elasticsearch.common.util.concurrent.ThreadContext +import tech.beshu.ror.accesscontrol.domain.Header +import tech.beshu.ror.es.handler.AclAwareRequestFilter.EsContext +import tech.beshu.ror.utils.JavaConverters + +import scala.language.implicitConversions + +final class ThreadContextOps(val threadContext: ThreadContext) extends AnyVal { + + def stashAndMergeResponseHeaders(esContext: EsContext): ThreadContext.StoredContext = { + val responseHeaders = + JavaConverters.flattenPair(threadContext.getResponseHeaders).toSet ++ esContext.threadContextResponseHeaders + val storedContext = threadContext.stashContext() + responseHeaders.foreach { case (k, v) => threadContext.addResponseHeader(k, v) } + storedContext + } + + def putHeaderIfNotPresent(header: Header): ThreadContext = { + Option(threadContext.getHeader(header.name.value.value)) match { + case Some(_) => + case None => threadContext.putHeader(header.name.value.value, header.value.value) + } + threadContext + } + + def addRorUserAuthenticationHeader(nodeName: String): ThreadContext = { + putHeaderIfNotPresent(XPackSecurityAuthenticationHeader.createRorUserAuthenticationHeader(nodeName)) + } + + def addXpackSecurityAuthenticationHeader(nodeName: String): ThreadContext = { + putHeaderIfNotPresent(XPackSecurityAuthenticationHeader.createXpackSecurityAuthenticationHeader(nodeName)) + } + + def addSystemAuthenticationHeader(nodeName: String): ThreadContext = { + putHeaderIfNotPresent(XPackSecurityAuthenticationHeader.createSystemAuthenticationHeader(nodeName)) + } +} + +object ThreadContextOps { + implicit def createThreadContextOps(threadContext: ThreadContext): ThreadContextOps = new ThreadContextOps(threadContext) +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/ThreadRepo.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/ThreadRepo.scala new file mode 100644 index 0000000000..367802f78a --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/ThreadRepo.scala @@ -0,0 +1,52 @@ +/* + * 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.utils + +import org.elasticsearch.rest.RestRequest +import tech.beshu.ror.accesscontrol.domain.UriPath +import tech.beshu.ror.es.RorRestChannel + +object ThreadRepo { + private val threadLocalChannel = new ThreadLocal[RorRestChannel] + + def setRestChannel(restChannel: RorRestChannel): Unit = { + threadLocalChannel.set(restChannel) + } + + def removeRestChannel(restChannel: RorRestChannel): Unit = { + if (threadLocalChannel.get() == restChannel) threadLocalChannel.remove() + } + + def getRorRestChannel: Option[RorRestChannel] = { + for { + channel <- Option(threadLocalChannel.get) + request <- Option(channel.request()) + } yield { + if(!shouldRemovingRestChannelBePostponedFor(request)) threadLocalChannel.remove() + channel + } + } + + private def shouldRemovingRestChannelBePostponedFor(request: RestRequest) = { + // because of the new implementation of RestTemplatesAction in ES 7.16.0 which don't take into consideration + // modification of ActionRequest, this workaround has to be introduced - we have to not remove the Rest Channel + // from the thread local, so the get composable templates request will be processed by ROR's ACL + UriPath + .from(request.uri()) + .exists(_.isCatTemplatePath) + } +} diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/XContentBuilderOps.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/XContentBuilderOps.scala new file mode 100644 index 0000000000..ce8bf23512 --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/XContentBuilderOps.scala @@ -0,0 +1,83 @@ +/* + * 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.utils + +import org.elasticsearch.xcontent.XContentBuilder +import ujson._ + +import scala.language.implicitConversions + +class XContentBuilderOps(val builder: XContentBuilder) extends AnyVal { + + def json(json: Value): XContentBuilder = { + applyJsonToBuilder(builder, None, json) + } + + private def applyJsonToBuilder(builder: XContentBuilder, + fieldName: Option[String], + json: Value): XContentBuilder = { + def startObject(currentBuilder: XContentBuilder) = fieldName match { + case Some(name) => currentBuilder.startObject(name) + case None => currentBuilder.startObject() + } + + def startArray(currentBuilder: XContentBuilder) = fieldName match { + case Some(name) => currentBuilder.startArray(name) + case None => currentBuilder.startArray() + } + + json match { + case Obj(map) => + map + .foldLeft(startObject(builder)) { + case (currentBuilder, (fieldName, fieldValue)) => + applyJsonToBuilder(currentBuilder, Some(fieldName), fieldValue) + } + .endObject() + case Arr(values) => + values + .foldLeft(startArray(builder)) { + case (currentBuilder, arrayObject) => + applyJsonToBuilder(currentBuilder, None, arrayObject) + } + .endArray() + case Str(value) => + fieldName match { + case Some(aKey) => builder.field(aKey, value) + case None => builder.value(value) + } + case Num(value) => + fieldName match { + case Some(aKey) => builder.field(aKey, value) + case None => builder.value(value) + } + case Bool(value) => + fieldName match { + case Some(aKey) => builder.field(aKey, value) + case None => builder.value(value) + } + case Null => + fieldName match { + case Some(aKey) => builder.nullField(aKey) + case None => builder.nullValue() + } + } + } +} +object XContentBuilderOps { + implicit def toXContentBuilderOps(builder: XContentBuilder): XContentBuilderOps = new XContentBuilderOps(builder) +} \ No newline at end of file diff --git a/es813x/src/main/scala/tech/beshu/ror/es/utils/XPackSecurityAuthenticationHeader.scala b/es813x/src/main/scala/tech/beshu/ror/es/utils/XPackSecurityAuthenticationHeader.scala new file mode 100644 index 0000000000..3183256a1b --- /dev/null +++ b/es813x/src/main/scala/tech/beshu/ror/es/utils/XPackSecurityAuthenticationHeader.scala @@ -0,0 +1,82 @@ +/* + * 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.utils + +import eu.timepit.refined.auto._ +import eu.timepit.refined.types.string.NonEmptyString +import org.elasticsearch.{TransportVersion, TransportVersions} +import org.elasticsearch.common.bytes.BytesReference +import org.elasticsearch.common.io.stream.BytesStreamOutput +import tech.beshu.ror.accesscontrol.domain.Header + +import java.util.Base64 +import scala.jdk.CollectionConverters._ + +object XPackSecurityAuthenticationHeader { + + def createRorUserAuthenticationHeader(nodeName: String) = new Header( + Header.Name("_xpack_security_authentication"), + getAuthenticationHeaderValue(nodeName, "ROR", isInternal = false) + ) + + def createXpackSecurityAuthenticationHeader(nodeName: String) = new Header( + Header.Name("_xpack_security_authentication"), + getAuthenticationHeaderValue(nodeName, "_xpack_security", isInternal = true) + ) + + def createSystemAuthenticationHeader(nodeName: String) = new Header( + Header.Name("_xpack_security_authentication"), + getAuthenticationHeaderValue(nodeName, "_system", isInternal = true) + ) + + private def getAuthenticationHeaderValue(nodeName: String, userName: String, isInternal: Boolean): NonEmptyString = { + val output = new BytesStreamOutput() + val currentVersion = TransportVersion.current() + output.setTransportVersion(currentVersion) + TransportVersion.writeVersion(currentVersion, output) + output.writeBoolean(isInternal) + if(isInternal) { + output.writeString(userName) + } else { + output.writeString(userName) + output.writeStringArray(Array("superuser")) + output.writeGenericMap(Map.empty[String, AnyRef].asJava) + output.writeOptionalString(null) + output.writeOptionalString(null) + output.writeBoolean(true) + output.writeBoolean(false) + } + output.writeString(nodeName) + output.writeString("__attach") + output.writeString("__attach") + if(output.getTransportVersion.onOrAfter(TransportVersions.V_8_2_0)) { + output.writeBoolean(false) + } + output.writeBoolean(false) + if (output.getTransportVersion.onOrAfter(TransportVersions.V_7_0_0)) { + output.writeVInt(4) // Internal + if(output.getTransportVersion.onOrAfter(TransportVersions.V_8_8_0)) { + output.writeVInt(0) + } else { + output.writeGenericMap(Map.empty[String, Object].asJava) + } + } + NonEmptyString.unsafeFrom { + Base64.getEncoder.encodeToString(BytesReference.toBytes(output.bytes())) + } + } +} diff --git a/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/Es813xPatch.scala b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/Es813xPatch.scala new file mode 100644 index 0000000000..34125e3032 --- /dev/null +++ b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/Es813xPatch.scala @@ -0,0 +1,49 @@ +/* + * 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.tools.core.patches + +import just.semver.SemVer +import tech.beshu.ror.tools.core.patches.base.TransportNetty4AwareEsPatch +import tech.beshu.ror.tools.core.patches.internal.RorPluginDirectory +import tech.beshu.ror.tools.core.patches.internal.filePatchers._ +import tech.beshu.ror.tools.core.patches.internal.modifiers.bytecodeJars._ +import tech.beshu.ror.tools.core.patches.internal.modifiers.securityPolicyFiles.AddCreateClassLoaderPermission + +import scala.language.postfixOps + +private[patches] class Es813xPatch(rorPluginDirectory: RorPluginDirectory, esVersion: SemVer) + extends TransportNetty4AwareEsPatch(rorPluginDirectory, esVersion, + new ElasticsearchJarPatchCreator( + OpenModule, + ModifyPolicyUtilClass, + new RepositoriesServiceAvailableForClusterServiceForAnyTypeOfNode(esVersion) + ), + new RorSecurityPolicyPatchCreator( + AddCreateClassLoaderPermission + ), + new XPackCoreJarPatchCreator( + OpenModule + ), + new XPackSecurityJarPatchCreator( + OpenModule, + DeactivateSecurityActionFilter, + DeactivateAuthenticationServiceInHttpTransport + ), + new XPackIlmJarPatchCreator( + OpenModule + ) + ) diff --git a/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/base/EsPatch.scala b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/base/EsPatch.scala index 2933d1fdcb..600adbea38 100644 --- a/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/base/EsPatch.scala +++ b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/base/EsPatch.scala @@ -89,6 +89,7 @@ object EsPatch { val rorPluginDirectory = new RorPluginDirectory(esDirectory) new EsPatchLoggingDecorator( readEsVersion(esDirectory) match { + case esVersion if esVersion >= es8130 => new Es813xPatch(rorPluginDirectory, esVersion) case esVersion if esVersion >= es890 => new Es89xPatch(rorPluginDirectory, esVersion) case esVersion if esVersion >= es830 => new Es83xPatch(rorPluginDirectory, esVersion) case esVersion if esVersion >= es800 => new Es80xPatch(rorPluginDirectory, esVersion) diff --git a/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/internal/filePatchers/XPackIlmJarPatchCreator.scala b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/internal/filePatchers/XPackIlmJarPatchCreator.scala new file mode 100644 index 0000000000..28b4c6fd15 --- /dev/null +++ b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/patches/internal/filePatchers/XPackIlmJarPatchCreator.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.tools.core.patches.internal.filePatchers + +import just.semver.SemVer +import tech.beshu.ror.tools.core.patches.internal.modifiers.FileModifier +import tech.beshu.ror.tools.core.patches.internal.{FilePatch, RorPluginDirectory} + +private[patches] class XPackIlmJarPatchCreator(patchingSteps: FileModifier*) + extends FilePatchCreator[XPackIlmJarPatch] { + + override def create(rorPluginDirectory: RorPluginDirectory, + esVersion: SemVer): XPackIlmJarPatch = + new XPackIlmJarPatch(rorPluginDirectory, esVersion, patchingSteps) +} + +private[patches] class XPackIlmJarPatch(rorPluginDirectory: RorPluginDirectory, + esVersion: SemVer, + patchingSteps: Iterable[FileModifier]) + extends FilePatch( + rorPluginDirectory = rorPluginDirectory, + fileToPatchPath = rorPluginDirectory.esDirectory.modulesPath / "x-pack-ilm" / s"x-pack-ilm-${esVersion.render}.jar", + patchingSteps = patchingSteps + ) \ No newline at end of file diff --git a/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/utils/EsUtil.scala b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/utils/EsUtil.scala index 2cda34c776..9f12c616e5 100644 --- a/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/utils/EsUtil.scala +++ b/ror-tools-core/src/main/scala/tech/beshu/ror/tools/core/utils/EsUtil.scala @@ -24,6 +24,7 @@ object EsUtil { private val elasticsearchJar = """^elasticsearch-(\d+\.\d+\.\d+)\.jar$""".r private val transportNetty4JarNameRegex = """^transport-netty4-\d+\.\d+\.\d+\.jar$""".r + val es8130: SemVer = SemVer.unsafeParse("8.13.0") val es890: SemVer = SemVer.unsafeParse("8.9.0") val es830: SemVer = SemVer.unsafeParse("8.3.0") val es820: SemVer = SemVer.unsafeParse("8.2.0") diff --git a/settings.gradle b/settings.gradle index 1dcd96bdfe..158ba50e79 100644 --- a/settings.gradle +++ b/settings.gradle @@ -57,6 +57,7 @@ include 'es89x' include 'es810x' include 'es811x' include 'es812x' +include 'es813x' include 'ror-tools-core' include 'ror-tools' include 'tests-utils'