From 5fb3bd81244ddf4484c9884ac50f5c00120f86a1 Mon Sep 17 00:00:00 2001 From: Maciej Cichanowicz Date: Fri, 24 May 2024 13:55:00 +0200 Subject: [PATCH] Move impersonation and anonymous access logic to AuthenticationManager --- .../ui/api/AppApiHttpService.scala | 8 +- .../nussknacker/ui/api/BaseHttpService.scala | 7 +- .../ui/api/ComponentApiHttpService.scala | 8 +- .../ui/api/DeploymentApiHttpService.scala | 8 +- .../ui/api/DictApiHttpService.scala | 8 +- .../ui/api/ManagementApiHttpService.scala | 8 +- .../ui/api/MigrationApiHttpService.scala | 10 +- .../ui/api/NodesApiHttpService.scala | 8 +- .../ui/api/NotificationApiHttpService.scala | 10 +- .../api/ScenarioActivityApiHttpService.scala | 10 +- .../ScenarioParametersApiHttpService.scala | 10 +- .../ui/api/StatisticsApiHttpService.scala | 8 +- .../ui/api/UserApiHttpService.scala | 8 +- .../server/AkkaHttpBasedRouteProvider.scala | 41 +++--- ...hAccessControlCheckingDesignerConfig.scala | 33 +++++ .../MigrationApiHttpServiceSecuritySpec.scala | 48 ++++++- .../api/NodesApiHttpServiceBusinessSpec.scala | 4 +- ...DesignerApiAvailableToExposeYamlSpec.scala | 2 - ...ioActivityApiHttpServiceSecuritySpec.scala | 52 +++++++ ...ParametersApiHttpServiceSecuritySpec.scala | 35 +++++ .../api/UserApiHttpServiceSecuritySpec.scala | 33 +++++ docs/MigrationGuide.md | 11 +- .../security/AuthCredentials.scala | 12 +- .../accesslogic/AnonymousAccess.scala | 28 ++++ .../accesslogic/ImpersonatedAccess.scala | 45 ++++++ .../ui/security/api/AuthenticatedUser.scala | 16 +++ .../security/api/AuthenticationManager.scala | 75 ++++++++++ .../api/AuthenticationResources.scala | 84 +----------- .../BasicAuthenticationResources.scala | 13 +- .../basicauth/BasicHttpAuthenticator.scala | 3 +- .../dummy/DummyAuthenticationResources.scala | 21 +-- .../OAuth2AuthenticationResources.scala | 13 +- .../api/AuthenticationManagerSpec.scala | 129 ++++++++++++++++++ .../basicauth/BasicAuthenticationSpec.scala | 20 --- .../dummy/DummyAuthenticationSpec.scala | 43 ------ 35 files changed, 620 insertions(+), 252 deletions(-) create mode 100644 security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/AnonymousAccess.scala create mode 100644 security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/ImpersonatedAccess.scala create mode 100644 security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManager.scala create mode 100644 security/src/test/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManagerSpec.scala delete mode 100644 security/src/test/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationSpec.scala diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/AppApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/AppApiHttpService.scala index d2cc2d81bd5..8c677ff7714 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/AppApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/AppApiHttpService.scala @@ -13,24 +13,24 @@ import pl.touk.nussknacker.ui.api.description.AppApiEndpoints.Dtos._ import pl.touk.nussknacker.ui.process.ProcessService.GetScenarioWithDetailsOptions import pl.touk.nussknacker.ui.process.processingtype.{ProcessingTypeDataProvider, ProcessingTypeDataReload} import pl.touk.nussknacker.ui.process.{ProcessService, ScenarioQuery} -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser, NussknackerInternalUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser, NussknackerInternalUser} import scala.concurrent.{ExecutionContext, Future} import scala.util.control.NonFatal class AppApiHttpService( config: Config, - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, processingTypeDataReloader: ProcessingTypeDataReload, modelBuildInfos: ProcessingTypeDataProvider[Map[String, String], _], categories: ProcessingTypeDataProvider[String, _], processService: ProcessService, shouldExposeConfig: Boolean )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val appApiEndpoints = new AppApiEndpoints(authenticator.authenticationMethod()) + private val appApiEndpoints = new AppApiEndpoints(authenticationManager.authenticationEndpointInput()) expose { appApiEndpoints.appHealthCheckEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala index 05667cd53e7..b3c8e5de935 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala @@ -6,7 +6,6 @@ import pl.touk.nussknacker.restmodel.SecurityError.{AuthenticationError, Authori import pl.touk.nussknacker.security.AuthCredentials import pl.touk.nussknacker.ui.api.BaseHttpService.{CustomAuthorizationError, NoRequirementServerEndpoint} import pl.touk.nussknacker.ui.security.api.CreationError.ImpersonationNotAllowed -import pl.touk.nussknacker.ui.security.api.LoggedUser.create import pl.touk.nussknacker.ui.security.api._ import sttp.tapir.server.{PartialServerEndpoint, ServerEndpoint} @@ -14,7 +13,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{ExecutionContext, Future} abstract class BaseHttpService( - authenticator: AuthenticationResources + authenticationManager: AuthenticationManager )(implicit executionContext: ExecutionContext) { // the discussion about this approach can be found here: https://github.com/TouK/nussknacker/pull/4685#discussion_r1329794444 @@ -51,13 +50,13 @@ abstract class BaseHttpService( protected def authorizeKnownUser[BUSINESS_ERROR]( credentials: AuthCredentials ): Future[LogicResult[BUSINESS_ERROR, LoggedUser]] = { - authenticator + authenticationManager .authenticate(credentials) .map { case Some(user) if user.roles.nonEmpty => // TODO: This is strange that we call authenticator.authenticate and the first thing that we do with the returned user is // creation of another user representation based on authenticator.configuration. Shouldn't we just return the LoggedUser? - LoggedUser.create(user, authenticator.configuration.rules) match { + LoggedUser.create(user, authenticationManager.authenticationRules) match { case Right(loggedUser) => success(loggedUser) case Left(ImpersonationNotAllowed) => securityError(AuthorizationError) } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ComponentApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ComponentApiHttpService.scala index 57396763b05..6020d081edb 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ComponentApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ComponentApiHttpService.scala @@ -4,18 +4,18 @@ import com.typesafe.scalalogging.LazyLogging import pl.touk.nussknacker.engine.api.component.DesignerWideComponentId import pl.touk.nussknacker.restmodel.component.ComponentApiEndpoints import pl.touk.nussknacker.ui.definition.component.ComponentService -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser} import scala.concurrent.ExecutionContext class ComponentApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, componentService: ComponentService )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val componentApiEndpoints = new ComponentApiEndpoints(authenticator.authenticationMethod()) + private val componentApiEndpoints = new ComponentApiEndpoints(authenticationManager.authenticationEndpointInput()) expose { componentApiEndpoints.componentsListEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpService.scala index 2553a3cb7d2..465f08f4920 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpService.scala @@ -3,17 +3,17 @@ package pl.touk.nussknacker.ui.api import pl.touk.nussknacker.ui.api.description.DeploymentApiEndpoints import pl.touk.nussknacker.ui.api.description.DeploymentApiEndpoints.Dtos._ import pl.touk.nussknacker.ui.process.newdeployment.{DeploymentService, RunDeploymentCommand} -import pl.touk.nussknacker.ui.security.api.AuthenticationResources +import pl.touk.nussknacker.ui.security.api.AuthenticationManager import scala.concurrent.ExecutionContext class DeploymentApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, deploymentService: DeploymentService )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) { + extends BaseHttpService(authenticationManager) { - private val endpoints = new DeploymentApiEndpoints(authenticator.authenticationMethod()) + private val endpoints = new DeploymentApiEndpoints(authenticationManager.authenticationEndpointInput()) expose { endpoints.runDeploymentEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala index 59e254fe327..cd0583a30b0 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala @@ -15,18 +15,18 @@ import pl.touk.nussknacker.ui.api.description.DictApiEndpoints.DictError.{ } import pl.touk.nussknacker.ui.api.description.DictApiEndpoints.Dtos.DictDto import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeDataProvider -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser} import scala.concurrent.{ExecutionContext, Future} class DictApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, processingTypeData: ProcessingTypeDataProvider[(DictQueryService, Map[String, DictDefinition], ClassLoader), _] )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val dictApiEndpoints = new DictApiEndpoints(authenticator.authenticationMethod()) + private val dictApiEndpoints = new DictApiEndpoints(authenticationManager.authenticationEndpointInput()) expose { dictApiEndpoints.dictionaryEntryQueryEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementApiHttpService.scala index cdc7a305378..e2e17ccf29a 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ManagementApiHttpService.scala @@ -14,20 +14,20 @@ import pl.touk.nussknacker.ui.api.ManagementApiEndpoints.ManagementApiError.{NoA import pl.touk.nussknacker.ui.api.{BaseHttpService, CustomActionValidationDto, ManagementApiEndpoints} import pl.touk.nussknacker.ui.process.ProcessService import pl.touk.nussknacker.ui.process.deployment.DeploymentManagerDispatcher -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser} import pl.touk.nussknacker.ui.validation.CustomActionValidator import scala.concurrent.{ExecutionContext, Future} class ManagementApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, dispatcher: DeploymentManagerDispatcher, processService: ProcessService )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val managementApiEndpoints = new ManagementApiEndpoints(authenticator.authenticationMethod()) + private val managementApiEndpoints = new ManagementApiEndpoints(authenticationManager.authenticationEndpointInput()) expose { managementApiEndpoints.customActionValidationEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpService.scala index 7415a308236..441d594668c 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpService.scala @@ -6,19 +6,21 @@ import pl.touk.nussknacker.ui.api.description.MigrationApiEndpoints import pl.touk.nussknacker.ui.api.description.MigrationApiEndpoints.Dtos._ import pl.touk.nussknacker.ui.migrations.MigrationService.MigrationError import pl.touk.nussknacker.ui.migrations.{MigrateScenarioData, MigrationApiAdapterService, MigrationService} -import pl.touk.nussknacker.ui.security.api.AuthenticationResources +import pl.touk.nussknacker.ui.security.api.AuthenticationManager import scala.concurrent.{ExecutionContext, Future} class MigrationApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, migrationService: MigrationService, migrationApiAdapterService: MigrationApiAdapterService )(implicit val ec: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val remoteEnvironmentApiEndpoints = new MigrationApiEndpoints(authenticator.authenticationMethod()) + private val remoteEnvironmentApiEndpoints = new MigrationApiEndpoints( + authenticationManager.authenticationEndpointInput() + ) expose { remoteEnvironmentApiEndpoints.migrateEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala index 0c40cc769c8..2d1b9b26f8e 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NodesApiHttpService.scala @@ -37,14 +37,14 @@ import pl.touk.nussknacker.ui.api.utils.ScenarioHttpServiceExtensions import pl.touk.nussknacker.ui.process.ProcessService import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.repository.ProcessDBQueryRepository.ProcessNotFoundError -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser} import pl.touk.nussknacker.ui.suggester.ExpressionSuggester import pl.touk.nussknacker.ui.validation.{NodeValidator, ParametersValidator, UIProcessValidator} import scala.concurrent.{ExecutionContext, Future} class NodesApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, processingTypeToConfig: ProcessingTypeDataProvider[ModelData, _], processingTypeToProcessValidator: ProcessingTypeDataProvider[UIProcessValidator, _], processingTypeToNodeValidator: ProcessingTypeDataProvider[NodeValidator, _], @@ -52,7 +52,7 @@ class NodesApiHttpService( processingTypeToParametersValidator: ProcessingTypeDataProvider[ParametersValidator, _], protected override val scenarioService: ProcessService )(override protected implicit val executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with ScenarioHttpServiceExtensions with LazyLogging { @@ -60,7 +60,7 @@ class NodesApiHttpService( override protected def noScenarioError(scenarioName: ProcessName): NodesError = NoScenario(scenarioName) override protected def noPermissionError: NodesError with CustomAuthorizationError = NoPermission - private val nodesApiEndpoints = new NodesApiEndpoints(authenticator.authenticationMethod()) + private val nodesApiEndpoints = new NodesApiEndpoints(authenticationManager.authenticationEndpointInput()) private val additionalInfoProviders = new AdditionalInfoProviders(processingTypeToConfig) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NotificationApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NotificationApiHttpService.scala index 898e0557221..601e97acff4 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NotificationApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/NotificationApiHttpService.scala @@ -3,18 +3,20 @@ package pl.touk.nussknacker.ui.api import com.typesafe.scalalogging.LazyLogging import pl.touk.nussknacker.ui.api.description.NotificationApiEndpoints import pl.touk.nussknacker.ui.notifications.NotificationService -import pl.touk.nussknacker.ui.security.api.AuthenticationResources +import pl.touk.nussknacker.ui.security.api.AuthenticationManager import scala.concurrent.ExecutionContext class NotificationApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, notificationService: NotificationService )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val notificationApiEndpoints = new NotificationApiEndpoints(authenticator.authenticationMethod()) + private val notificationApiEndpoints = new NotificationApiEndpoints( + authenticationManager.authenticationEndpointInput() + ) expose { notificationApiEndpoints.notificationEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala index 47f5a9f3b78..f0232e29067 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala @@ -14,7 +14,7 @@ import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos. import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos._ import pl.touk.nussknacker.ui.process.repository.{ProcessActivityRepository, UserComment} import pl.touk.nussknacker.ui.process.{ProcessService, ScenarioAttachmentService} -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser} import pl.touk.nussknacker.ui.server.HeadersSupport.ContentDisposition import pl.touk.nussknacker.ui.server.TapirStreamEndpointProvider import sttp.model.MediaType @@ -24,17 +24,19 @@ import java.net.URLConnection import scala.concurrent.{ExecutionContext, Future} class ScenarioActivityApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, scenarioActivityRepository: ProcessActivityRepository, scenarioService: ProcessService, scenarioAuthorizer: AuthorizeProcess, attachmentService: ScenarioAttachmentService, streamEndpointProvider: TapirStreamEndpointProvider )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val scenarioActivityApiEndpoints = new ScenarioActivityApiEndpoints(authenticator.authenticationMethod()) + private val scenarioActivityApiEndpoints = new ScenarioActivityApiEndpoints( + authenticationManager.authenticationEndpointInput() + ) expose { scenarioActivityApiEndpoints.scenarioActivityEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpService.scala index 983f9be20d4..962c84afe6e 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpService.scala @@ -4,18 +4,20 @@ import com.typesafe.scalalogging.LazyLogging import pl.touk.nussknacker.ui.api.description.ScenarioParametersApiEndpoints import pl.touk.nussknacker.ui.api.description.ScenarioParametersApiEndpoints.Dtos.ScenarioParametersCombinationWithEngineErrors import pl.touk.nussknacker.ui.process.processingtype.{ProcessingTypeDataProvider, ScenarioParametersService} -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser} import scala.concurrent.{ExecutionContext, Future} class ScenarioParametersApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, scenarioParametersService: ProcessingTypeDataProvider[_, ScenarioParametersService] )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val parametersApiEndpoints = new ScenarioParametersApiEndpoints(authenticator.authenticationMethod()) + private val parametersApiEndpoints = new ScenarioParametersApiEndpoints( + authenticationManager.authenticationEndpointInput() + ) expose { parametersApiEndpoints.scenarioParametersCombinationsEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/StatisticsApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/StatisticsApiHttpService.scala index 5fcafdfc2d3..473fe335c0a 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/StatisticsApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/StatisticsApiHttpService.scala @@ -9,20 +9,20 @@ import pl.touk.nussknacker.ui.api.description.StatisticsApiEndpoints.Dtos.{ StatisticUrlResponseDto } import pl.touk.nussknacker.ui.db.timeseries.{FEStatisticsRepository, WriteFEStatisticsRepository} -import pl.touk.nussknacker.ui.security.api.AuthenticationResources +import pl.touk.nussknacker.ui.security.api.AuthenticationManager import pl.touk.nussknacker.ui.statistics.UsageStatisticsReportsSettingsService import scala.concurrent.{ExecutionContext, Future} class StatisticsApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, usageStatisticsReportsSettingsService: UsageStatisticsReportsSettingsService, repository: FEStatisticsRepository[Future] )(implicit ec: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val endpoints = new StatisticsApiEndpoints(authenticator.authenticationMethod()) + private val endpoints = new StatisticsApiEndpoints(authenticationManager.authenticationEndpointInput()) private val ignoringErrorsRepository = new IgnoringErrorsStatisticsRepository(repository) expose { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/UserApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/UserApiHttpService.scala index b6cd20718c8..6a6ba73b84f 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/UserApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/UserApiHttpService.scala @@ -3,18 +3,18 @@ package pl.touk.nussknacker.ui.api import com.typesafe.scalalogging.LazyLogging import pl.touk.nussknacker.ui.api.description.{DisplayableUser, UserApiEndpoints} import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeDataProvider -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser} +import pl.touk.nussknacker.ui.security.api.{AuthenticationManager, LoggedUser} import scala.concurrent.{ExecutionContext, Future} class UserApiHttpService( - authenticator: AuthenticationResources, + authenticationManager: AuthenticationManager, categories: ProcessingTypeDataProvider[String, _], )(implicit executionContext: ExecutionContext) - extends BaseHttpService(authenticator) + extends BaseHttpService(authenticationManager) with LazyLogging { - private val userApiEndpoints = new UserApiEndpoints(authenticator.authenticationMethod()) + private val userApiEndpoints = new UserApiEndpoints(authenticationManager.authenticationEndpointInput()) expose { userApiEndpoints.userInfoEndpoint diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala index 562fa12fdf2..8205deda007 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala @@ -25,7 +25,6 @@ import pl.touk.nussknacker.engine.util.multiplicity.{Empty, Many, Multiplicity, import pl.touk.nussknacker.engine.{ConfigWithUnresolvedVersion, DeploymentManagerDependencies, ModelDependencies} import pl.touk.nussknacker.processCounts.influxdb.InfluxCountsReporterCreator import pl.touk.nussknacker.processCounts.{CountsReporter, CountsReporterCreator} -import pl.touk.nussknacker.restmodel.SecurityError.AuthorizationError import pl.touk.nussknacker.ui.api._ import pl.touk.nussknacker.ui.config.scenariotoolbar.CategoriesScenarioToolbarsConfigParser import pl.touk.nussknacker.ui.config.{ @@ -67,7 +66,12 @@ import pl.touk.nussknacker.ui.process.processingtype.{ProcessingTypeData, Proces import pl.touk.nussknacker.ui.process.repository._ import pl.touk.nussknacker.ui.process.test.{PreliminaryScenarioTestDataSerDe, ScenarioTestService} import pl.touk.nussknacker.ui.processreport.ProcessCounter -import pl.touk.nussknacker.ui.security.api.{AuthenticationResources, LoggedUser, NussknackerInternalUser} +import pl.touk.nussknacker.ui.security.api.{ + AuthenticationManager, + AuthenticationResources, + LoggedUser, + NussknackerInternalUser +} import pl.touk.nussknacker.ui.services.{ManagementApiHttpService, NuDesignerExposedApiHttpService} import pl.touk.nussknacker.ui.statistics.repository.FingerprintRepositoryImpl import pl.touk.nussknacker.ui.statistics.{FingerprintService, UsageStatisticsReportsSettingsService} @@ -204,6 +208,7 @@ class AkkaHttpBasedRouteProvider( val processActivityRepository = new DbProcessActivityRepository(dbRef) val authenticationResources = AuthenticationResources(resolvedConfig, getClass.getClassLoader, sttpBackend) + val authenticationManager = new AuthenticationManager(authenticationResources) Initialization.init(migrations, dbRef, processRepository, environment) @@ -255,7 +260,7 @@ class AkkaHttpBasedRouteProvider( val processAuthorizer = new AuthorizeProcess(futureProcessRepository) val appApiHttpService = new AppApiHttpService( config = resolvedConfig, - authenticator = authenticationResources, + authenticationManager = authenticationManager, processingTypeDataReloader = processingTypeDataProvider, modelBuildInfos = modelBuildInfo, categories = processingTypeDataProvider.mapValues(_.category), @@ -277,32 +282,32 @@ class AkkaHttpBasedRouteProvider( ) val migrationApiHttpService = new MigrationApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, migrationService = migrationService, migrationApiAdapterService = migrationApiAdapterService ) val componentsApiHttpService = new ComponentApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, componentService = componentService ) val userApiHttpService = new UserApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, categories = processingTypeDataProvider.mapValues(_.category) ) val managementApiHttpService = new ManagementApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, dispatcher = dmDispatcher, processService = processService ) val notificationApiHttpService = new NotificationApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, notificationService = notificationService ) val nodesApiHttpService = new NodesApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, processingTypeToConfig = processingTypeDataProvider.mapValues(_.designerModelData.modelData), processingTypeToProcessValidator = processValidator, processingTypeToNodeValidator = processingTypeDataProvider.mapValues(v => @@ -318,7 +323,7 @@ class AkkaHttpBasedRouteProvider( ) val scenarioActivityApiHttpService = new ScenarioActivityApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, scenarioActivityRepository = processActivityRepository, scenarioService = processService, scenarioAuthorizer = processAuthorizer, @@ -329,11 +334,11 @@ class AkkaHttpBasedRouteProvider( new AkkaHttpBasedTapirStreamEndpointProvider() ) val scenarioParametersHttpService = new ScenarioParametersApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, scenarioParametersService = processingTypeDataProvider.mapCombined(_.parametersService) ) val dictApiHttpService = new DictApiHttpService( - authenticator = authenticationResources, + authenticationManager = authenticationManager, processingTypeData = processingTypeDataProvider.mapValues { processingTypeData => ( processingTypeData.designerModelData.modelData.designerDictServices.dictQueryService, @@ -353,7 +358,7 @@ class AkkaHttpBasedRouteProvider( dbioRunner, Clock.systemDefaultZone() ) - new DeploymentApiHttpService(authenticationResources, deploymentService) + new DeploymentApiHttpService(authenticationManager, deploymentService) } initMetrics(metricsRegistry, resolvedConfig, futureProcessRepository) @@ -438,7 +443,7 @@ class AkkaHttpBasedRouteProvider( ) val statisticsApiHttpService = new StatisticsApiHttpService( - authenticationResources, + authenticationManager, usageStatisticsReportsSettingsService, feStatisticsRepository ) @@ -477,7 +482,7 @@ class AkkaHttpBasedRouteProvider( createAppRoute( resolvedConfig = resolvedConfig, - authenticationResources = authenticationResources, + authenticationManager = authenticationManager, tapirRelatedRoutes = akkaHttpServerInterpreter.toRoute(nuDesignerApi.allEndpoints) :: Nil, apiResourcesWithAuthentication = apiResourcesWithAuthentication, apiResourcesWithoutAuthentication = apiResourcesWithoutAuthentication, @@ -506,7 +511,7 @@ class AkkaHttpBasedRouteProvider( private def createAppRoute( resolvedConfig: Config, - authenticationResources: AuthenticationResources, + authenticationManager: AuthenticationManager, tapirRelatedRoutes: List[Route], apiResourcesWithAuthentication: List[RouteWithUser], apiResourcesWithoutAuthentication: List[Route], @@ -521,11 +526,11 @@ class AkkaHttpBasedRouteProvider( } ~ pathPrefix("api") { apiResourcesWithoutAuthentication.reduce(_ ~ _) } ~ pathPrefix("api") { - authenticationResources.authenticate() { authenticatedUser => + authenticationManager.authenticate() { authenticatedUser => authorize(authenticatedUser.roles.nonEmpty) { val maybeLoggedUser = LoggedUser.create( authenticatedUser, - authenticationResources.configuration.rules + authenticationManager.authenticationRules ) authorize(maybeLoggedUser.isRight) { apiResourcesWithAuthentication diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/config/WithAccessControlCheckingDesignerConfig.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/config/WithAccessControlCheckingDesignerConfig.scala index 1e1096f7930..2545a648c43 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/config/WithAccessControlCheckingDesignerConfig.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/config/WithAccessControlCheckingDesignerConfig.scala @@ -5,6 +5,7 @@ import enumeratum.{Enum, EnumEntry} import io.restassured.specification.RequestSpecification import org.scalatest.{BeforeAndAfterAll, Suite} import pl.touk.nussknacker.engine.util.config.ScalaMajorVersionConfig +import pl.touk.nussknacker.security.ImpersonatedUserData import pl.touk.nussknacker.test.NuRestAssureExtensions import pl.touk.nussknacker.test.config.WithAccessControlCheckingDesignerConfig.TestCategory import pl.touk.nussknacker.test.utils.DesignerTestConfigValidator @@ -117,4 +118,36 @@ trait WithAccessControlCheckingConfigRestAssuredUsersExtensions extends NuRestAs requestSpecification.preemptiveBasicAuth("unknownuser", "wrongcredentials") } + implicit class UsersImpersonation[T <: RequestSpecification](requestSpecification: T) { + + import io.circe.syntax._ + + private val impersonationHeader = "Impersonate-User-Data" + + def impersonateReaderUser(): RequestSpecification = + requestSpecification.header( + impersonationHeader, + ImpersonatedUserData("reader", "reader", Set("Reader")).asJson.noSpaces + ) + + def impersonateLimitedReaderUser(): RequestSpecification = + requestSpecification.header( + impersonationHeader, + ImpersonatedUserData("limitedReader", "limitedReader", Set("LimitedReader")).asJson.noSpaces + ) + + def impersonateWriterUser(): RequestSpecification = + requestSpecification.header( + impersonationHeader, + ImpersonatedUserData("writer", "writer", Set("Writer")).asJson.noSpaces + ) + + def impersonateLimitedWriterUser(): RequestSpecification = + requestSpecification.header( + impersonationHeader, + ImpersonatedUserData("limitedWriter", "limitedWriter", Set("LimitedWriter")).asJson.noSpaces + ) + + } + } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpServiceSecuritySpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpServiceSecuritySpec.scala index c49edbd9845..87400d0c884 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpServiceSecuritySpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/MigrationApiHttpServiceSecuritySpec.scala @@ -70,7 +70,7 @@ class MigrationApiHttpServiceSecuritySpec .equalsPlainBody("The supplied user [reader] is not authorized to access this resource") } } - "no credentials were passes should" - { + "no credentials were passed should" - { "forbid access" in { given() .applicationState( @@ -85,6 +85,52 @@ class MigrationApiHttpServiceSecuritySpec .equalsPlainBody("The supplied user [anonymous] is not authorized to access this resource") } } + "impersonating user has permission to impersonate should" - { + "allow migration for impersonated user with appropriate permissions" in { + given() + .applicationState( + createSavedScenario(exampleScenario, Category1) + ) + .when() + .basicAuthAllPermUser() + .impersonateWriterUser() + .jsonBody(requestData) + .post(s"$nuDesignerHttpAddress/api/migrate") + .Then() + .statusCode(200) + .equalsPlainBody("") + } + "forbid access for impersonated user with limited reading permissions" in { + given() + .applicationState( + createSavedScenario(exampleScenario, Category1) + ) + .when() + .basicAuthAllPermUser() + .impersonateReaderUser() + .jsonBody(requestData) + .post(s"$nuDesignerHttpAddress/api/migrate") + .Then() + .statusCode(401) + .equalsPlainBody("The supplied user [reader] is not authorized to access this resource") + } + } + "impersonating user does not have permission to impersonate should" - { + "forbid access" in { + given() + .applicationState( + createSavedScenario(exampleScenario, Category1) + ) + .when() + .basicAuthWriter() + .impersonateWriterUser() + .jsonBody(requestData) + .post(s"$nuDesignerHttpAddress/api/migrate") + .Then() + .statusCode(403) + .equalsPlainBody("The supplied authentication is not authorized to access this resource") + } + } } private lazy val sourceEnvironmentId = "DEV" diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NodesApiHttpServiceBusinessSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NodesApiHttpServiceBusinessSpec.scala index ba34febde2d..1950a53b795 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NodesApiHttpServiceBusinessSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NodesApiHttpServiceBusinessSpec.scala @@ -504,7 +504,7 @@ class NodesApiHttpServiceBusinessSpec .applicationState { createSavedScenario(exampleScenario) } - .basicAuth("allpermuser", "allpermuser") + .basicAuthAllPermUser() .jsonBody(exampleNodeValidationRequestForFragment(fragmentName)) .when() .post(s"$nuDesignerHttpAddress/api/nodes/${exampleScenario.name}/validation") @@ -852,7 +852,7 @@ class NodesApiHttpServiceBusinessSpec "return 404 for not existent processing type" in { val notExistentProcessingType = "not-existent" given() - .basicAuth("allpermuser", "allpermuser") + .basicAuthAllPermUser() .jsonBody(exampleParametersValidationRequestBody) .when() .post(s"$nuDesignerHttpAddress/api/parameters/$notExistentProcessingType/validate") diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index f8d69477a52..80cf8fe20d8 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -7,7 +7,6 @@ import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions import pl.touk.nussknacker.security.AuthCredentials import pl.touk.nussknacker.test.utils.domain.ReflectionBasedUtils import pl.touk.nussknacker.test.utils.{InvalidExample, OpenAPIExamplesValidator, OpenAPISchemaComponents} -import pl.touk.nussknacker.ui.security.api.AnonymousAccess import pl.touk.nussknacker.ui.services.NuDesignerExposedApiHttpService import pl.touk.nussknacker.ui.util.Project import sttp.apispec.openapi.circe.yaml.RichOpenAPI @@ -164,7 +163,6 @@ object NuDesignerApiAvailableToExpose { private def createInstanceOf(clazz: Class[_ <: BaseEndpointDefinitions]) = { val basicAuth = auth .basic[Option[String]]() - .map { AnonymousAccess.optionalStringToAuthCredentialsMapping(false) } Try(clazz.getConstructor(classOf[EndpointInput[AuthCredentials]])) .map(_.newInstance(basicAuth)) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceSecuritySpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceSecuritySpec.scala index 6d18bfd583c..e6b37a116c1 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceSecuritySpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceSecuritySpec.scala @@ -186,6 +186,58 @@ class ScenarioActivityApiHttpServiceSecuritySpec .equalsPlainBody("The supplied authentication is not authorized to access this resource") } } + "impersonating user has permission to impersonate should" - { + "allow to add comment in scenario in allowed category for the impersonated user" in { + val allowedScenarioName = "s1" + given() + .applicationState { + createSavedScenario(exampleScenario(allowedScenarioName), category = Category1) + createSavedScenario(exampleScenario("s2"), category = Category2) + } + .when() + .basicAuthAllPermUser() + .impersonateLimitedWriterUser() + .plainBody(commentContent) + .post(s"$nuDesignerHttpAddress/api/processes/$allowedScenarioName/1/activity/comments") + .Then() + .statusCode(200) + .equalsPlainBody("") + } + "forbid to add comment in scenario because impersonated user has no writer permission" in { + val allowedScenarioName = "s1" + given() + .applicationState { + createSavedScenario(exampleScenario(allowedScenarioName), category = Category1) + createSavedScenario(exampleScenario("s2"), category = Category2) + } + .plainBody(commentContent) + .basicAuthAllPermUser() + .impersonateLimitedReaderUser() + .when() + .post(s"$nuDesignerHttpAddress/api/processes/$allowedScenarioName/1/activity/comments") + .Then() + .statusCode(403) + .equalsPlainBody("The supplied authentication is not authorized to access this resource") + } + } + "impersonating user does not have permission to impersonate should" - { + "forbid access" in { + val allowedScenarioName = "s1" + given() + .applicationState { + createSavedScenario(exampleScenario(allowedScenarioName), category = Category1) + createSavedScenario(exampleScenario("s2"), category = Category2) + } + .plainBody(commentContent) + .basicAuthWriter() + .impersonateLimitedReaderUser() + .when() + .post(s"$nuDesignerHttpAddress/api/processes/$allowedScenarioName/1/activity/comments") + .Then() + .statusCode(403) + .equalsPlainBody("The supplied authentication is not authorized to access this resource") + } + } } "The scenario remove comment endpoint when" - { diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpServiceSecuritySpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpServiceSecuritySpec.scala index 5105d55e390..7752f9b28ec 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpServiceSecuritySpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioParametersApiHttpServiceSecuritySpec.scala @@ -107,6 +107,41 @@ class ScenarioParametersApiHttpServiceSecuritySpec ) } } + "impersonating user has permission to impersonate should" - { + "return parameters combination for categories the impersonated user has a write access" in { + given() + .when() + .basicAuthAllPermUser() + .impersonateLimitedWriterUser() + .get(s"$nuDesignerHttpAddress/api/scenarioParametersCombinations") + .Then() + .statusCode(200) + .equalsJsonBody( + s"""{ + | "combinations": [ + | { + | "processingMode": "Unbounded-Stream", + | "category": "Category1", + | "engineSetupName": "Flink" + | } + | ], + | "engineSetupErrors": {} + |}""".stripMargin + ) + } + } + "impersonating user does not have permission to impersonate should" - { + "forbid access" in { + given() + .when() + .basicAuthWriter() + .impersonateLimitedWriterUser() + .get(s"$nuDesignerHttpAddress/api/scenarioParametersCombinations") + .Then() + .statusCode(403) + .equalsPlainBody("The supplied authentication is not authorized to access this resource") + } + } } } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/UserApiHttpServiceSecuritySpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/UserApiHttpServiceSecuritySpec.scala index 3d8c3ef87b3..543eebc7994 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/UserApiHttpServiceSecuritySpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/UserApiHttpServiceSecuritySpec.scala @@ -71,6 +71,39 @@ class UserApiHttpServiceSecuritySpec | "globalPermissions": [] |}""".stripMargin) } + "impersonating user has permission to impersonate should" - { + "return impersonated user info" in { + given() + .when() + .basicAuthAllPermUser() + .impersonateLimitedReaderUser() + .get(s"$nuDesignerHttpAddress/api/user") + .Then() + .statusCode(200) + .equalsJsonBody(s"""{ + | "id": "limitedReader", + | "username": "limitedReader", + | "isAdmin": false, + | "categories": [ "Category1" ], + | "categoryPermissions": { + | "Category1": [ "Read" ] + | }, + | "globalPermissions": [] + |}""".stripMargin) + } + } + "impersonating user does not have permission to impersonate should" - { + "forbid access" in { + given() + .when() + .basicAuthWriter() + .impersonateLimitedReaderUser() + .get(s"$nuDesignerHttpAddress/api/user") + .Then() + .statusCode(403) + .body(equalTo("The supplied authentication is not authorized to access this resource")) + } + } } "anonymous user credentials are passed directly should not authenticate the request" in { given() diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index 095b7c404cb..d5def1a7d7f 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -23,7 +23,16 @@ To see the biggest differences please consult the [changelog](Changelog.md). ### Code API changes -* [#6053](https://github.com/TouK/nussknacker/pull/6053) `OverrideUsername` permission was renamed as `Impersonate` and is now used as a global permission. +* [#6053](https://github.com/TouK/nussknacker/pull/6053) Added impersonation mechanism: + * `OverrideUsername` permission was renamed as `Impersonate` and is now used as a global permission. + * `AuthenticationManager` is now responsible for authentication. `AuthenticationResources` handles only plugin specific authentication now + and both anonymous access and impersonation access are handled at `AuthenticationManager` level. This leads to following changes + in `AuthenticationResources` API: + * `authenticate()` returns `AuthenticationDirective[AuthenticatedUser]` and not `Directive1[AuthenticatedUser]` + * `authenticate(authCredentials)` receives `PassedAuthCredentials` parameter type instead of `AuthCredentials` + as anonymous access is no longer part of `AuthenticationResources` logic + * `authenticationMethod()` returns `EndpointInput[Option[String]]` instead of `EndpointInput[AuthCredentials]`. + The `Option[String]` should hold the value that will be passed to the mentioned `PassedAuthCredentials`. * [#5609](https://github.com/TouK/nussknacker/pull/5609) [#5795](https://github.com/TouK/nussknacker/pull/5795) [#5837](https://github.com/TouK/nussknacker/pull/5837) [#5798](https://github.com/TouK/nussknacker/pull/5798) Refactoring around DeploymentManager's actions: * Custom Actions * `CustomAction`, `CustomActionParameter` and `CustomActionResult` moved from `extension-api` to `deployment-manager-api` module diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/security/AuthCredentials.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/security/AuthCredentials.scala index 15974b83012..b12de52a72c 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/security/AuthCredentials.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/security/AuthCredentials.scala @@ -1,10 +1,20 @@ package pl.touk.nussknacker.security +import io.circe.generic.JsonCodec + sealed trait AuthCredentials object AuthCredentials { final case class PassedAuthCredentials(value: String) extends AuthCredentials - case object AnonymousAccess extends AuthCredentials + + final case class ImpersonatedAuthCredentials( + impersonatingUserCredentials: PassedAuthCredentials, + impersonatedUserData: ImpersonatedUserData + ) extends AuthCredentials + + case object NoCredentialsProvided extends AuthCredentials } + +@JsonCodec final case class ImpersonatedUserData(id: String, username: String, roles: Set[String]) diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/AnonymousAccess.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/AnonymousAccess.scala new file mode 100644 index 00000000000..1ebdc1bd021 --- /dev/null +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/AnonymousAccess.scala @@ -0,0 +1,28 @@ +package pl.touk.nussknacker.ui.security.accesslogic + +import akka.http.scaladsl.server.Directives.{handleRejections, reject} +import akka.http.scaladsl.server.{AuthorizationFailedRejection, Directive0, RejectionHandler} +import pl.touk.nussknacker.ui.security.api.{AuthenticatedUser, AuthenticationResources} + +import scala.concurrent.Future + +class AnonymousAccess(authenticationResources: AuthenticationResources) { + + val anonymousUserRole: Option[String] = authenticationResources.configuration.anonymousUserRole + + def handleAuthorizationFailedRejection: Directive0 = handleRejections( + RejectionHandler + .newBuilder() + // If the authorization rejection was caused by anonymous access, + // we issue the Unauthorized status code with a challenge instead of the Forbidden + .handle { case AuthorizationFailedRejection => authenticationResources.authenticate() { _ => reject } } + .result() + ) + + def authenticateWithAnonymousAccess(): Future[Option[AuthenticatedUser]] = anonymousUserRole + .map { role => + Future.successful(Some(AuthenticatedUser.createAnonymousUser(Set(role)))) + } + .getOrElse { Future.successful(None) } + +} diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/ImpersonatedAccess.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/ImpersonatedAccess.scala new file mode 100644 index 00000000000..dc61ffe685c --- /dev/null +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/accesslogic/ImpersonatedAccess.scala @@ -0,0 +1,45 @@ +package pl.touk.nussknacker.ui.security.accesslogic + +import akka.http.scaladsl.server.Directive1 +import akka.http.scaladsl.server.Directives.{failWith, optionalHeaderValueByName, provide} +import io.circe.parser.decode +import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials +import pl.touk.nussknacker.security.ImpersonatedUserData +import pl.touk.nussknacker.ui.security.accesslogic.ImpersonatedAccess.impersonateHeaderName +import pl.touk.nussknacker.ui.security.api.{AuthenticatedUser, AuthenticationResources} +import sttp.tapir.{EndpointIO, header} + +import scala.concurrent.{ExecutionContext, Future} + +class ImpersonatedAccess(authenticationResources: AuthenticationResources) { + + def provideOrImpersonateUser(user: AuthenticatedUser): Directive1[AuthenticatedUser] = { + optionalHeaderValueByName(impersonateHeaderName).flatMap { + case Some(impersonatedUserData) => + decode[ImpersonatedUserData](impersonatedUserData) match { + case Right(userData) => provide(AuthenticatedUser.createImpersonatedUser(user, userData)) + case Left(error) => failWith(error) + } + case None => provide(user) + } + } + + def authenticateWithImpersonation( + impersonatingUserAuthCredentials: PassedAuthCredentials, + impersonatedUserData: ImpersonatedUserData + )(implicit ec: ExecutionContext): Future[Option[AuthenticatedUser]] = { + for { + impersonatingAuthUser <- authenticationResources.authenticate(impersonatingUserAuthCredentials) + impersonatedAuthenticatedUser <- impersonatingAuthUser match { + case Some(user) => Future.successful(Some(AuthenticatedUser.createImpersonatedUser(user, impersonatedUserData))) + case None => Future.successful(None) + } + } yield impersonatedAuthenticatedUser + } + +} + +object ImpersonatedAccess { + val impersonateHeaderName = "Impersonate-User-Data" + val headerEndpointInput: EndpointIO.Header[Option[String]] = header[Option[String]](impersonateHeaderName) +} diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticatedUser.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticatedUser.scala index 980e73dbc2a..bc71304fd8a 100644 --- a/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticatedUser.scala +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticatedUser.scala @@ -1,5 +1,7 @@ package pl.touk.nussknacker.ui.security.api +import pl.touk.nussknacker.security.ImpersonatedUserData + final case class AuthenticatedUser( id: String, username: String, @@ -10,4 +12,18 @@ final case class AuthenticatedUser( object AuthenticatedUser { def createAnonymousUser(roles: Set[String]): AuthenticatedUser = AuthenticatedUser("anonymous", "anonymous", roles) + + def createImpersonatedUser( + impersonatingUser: AuthenticatedUser, + impersonatedUserData: ImpersonatedUserData + ): AuthenticatedUser = + AuthenticatedUser( + impersonatingUser.id, + impersonatingUser.username, + impersonatingUser.roles, + Some(AuthenticatedUser(impersonatedUserData)) + ) + + def apply(userData: ImpersonatedUserData): AuthenticatedUser = + AuthenticatedUser(userData.id, userData.username, userData.roles) } diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManager.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManager.scala new file mode 100644 index 00000000000..9489ab64942 --- /dev/null +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManager.scala @@ -0,0 +1,75 @@ +package pl.touk.nussknacker.ui.security.api + +import akka.http.scaladsl.server.Directive1 +import io.circe.parser._ +import io.circe.syntax._ +import pl.touk.nussknacker.security.AuthCredentials.{ + ImpersonatedAuthCredentials, + NoCredentialsProvided, + PassedAuthCredentials +} +import pl.touk.nussknacker.security.{AuthCredentials, ImpersonatedUserData} +import pl.touk.nussknacker.ui.security.accesslogic.{AnonymousAccess, ImpersonatedAccess} +import sttp.tapir._ + +import scala.concurrent.{ExecutionContext, Future} + +class AuthenticationManager(authenticationResources: AuthenticationResources) { + + lazy val authenticationRules: List[AuthenticationConfiguration.ConfigRule] = + authenticationResources.configuration.rules + private val anonymousAccess = new AnonymousAccess(authenticationResources) + private val impersonatedAccess = new ImpersonatedAccess(authenticationResources) + + def authenticate(): Directive1[AuthenticatedUser] = { + anonymousAccess.anonymousUserRole match { + case Some(role) => + authenticationResources.authenticate().optional.flatMap { + case Some(user) => impersonatedAccess.provideOrImpersonateUser(user) + case None => + anonymousAccess.handleAuthorizationFailedRejection.tmap(_ => + AuthenticatedUser.createAnonymousUser(Set(role)) + ) + } + case None => authenticationResources.authenticate().flatMap(impersonatedAccess.provideOrImpersonateUser) + } + } + + def authenticate( + authCredentials: AuthCredentials + )(implicit ec: ExecutionContext): Future[Option[AuthenticatedUser]] = { + authCredentials match { + case passedCredentials @ PassedAuthCredentials(_) => authenticationResources.authenticate(passedCredentials) + case NoCredentialsProvided => anonymousAccess.authenticateWithAnonymousAccess() + case ImpersonatedAuthCredentials(impersonatingUserAuthCredentials, impersonatedUserData) => + impersonatedAccess.authenticateWithImpersonation(impersonatingUserAuthCredentials, impersonatedUserData) + } + } + + def authenticationEndpointInput(): EndpointInput[AuthCredentials] = { + authenticationResources + .authenticationMethod() + .and(ImpersonatedAccess.headerEndpointInput) + .map( + Mapping.fromDecode[(Option[String], Option[String]), AuthCredentials] { + case (Some(passedCredentials), None) => DecodeResult.Value(PassedAuthCredentials(passedCredentials)) + case (Some(passedCredentials), Some(impersonatedUserData)) => + decode[ImpersonatedUserData](impersonatedUserData) match { + case Right(impersonatedUserData) => + DecodeResult.Value( + ImpersonatedAuthCredentials(PassedAuthCredentials(passedCredentials), impersonatedUserData) + ) + case Left(error) => DecodeResult.Error(impersonatedUserData, error) + } + case (None, None) => DecodeResult.Value(NoCredentialsProvided) + case _ => DecodeResult.Missing + } { + case PassedAuthCredentials(value) => (Some(value), None) + case NoCredentialsProvided => (None, None) + case ImpersonatedAuthCredentials(impersonating, impersonated) => + (Some(impersonating.value), Some(impersonated.asJson.noSpaces)) + } + ) + } + +} diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationResources.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationResources.scala index dbd69e401ff..d68b7119bbf 100644 --- a/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationResources.scala +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/api/AuthenticationResources.scala @@ -5,17 +5,16 @@ import akka.http.scaladsl.server._ import akka.http.scaladsl.server.directives.AuthenticationDirective import com.typesafe.config.Config import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport -import pl.touk.nussknacker.security.AuthCredentials import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials -import pl.touk.nussknacker.ui.security.api.AnonymousAccess.optionalStringToAuthCredentialsMapping import sttp.client3.SttpBackend -import sttp.tapir.{DecodeResult, EndpointInput, Mapping} +import sttp.tapir.EndpointInput import scala.concurrent.{ExecutionContext, Future} trait AuthenticationResources extends Directives with FailFastCirceSupport { type CONFIG <: AuthenticationConfiguration + type Credentials = Option[String] def name: String def configuration: CONFIG @@ -29,11 +28,11 @@ trait AuthenticationResources extends Directives with FailFastCirceSupport { // Currently, in the implementation of `authenticate(authCredentials: AuthCredentials)` we use Akka HTTP classes, // so before we throw away Akka HTTP, we should migrate to some other implementations (e.g. from the Tapir's server // interpreter) or create our own. - def authenticate(): Directive1[AuthenticatedUser] + def authenticate(): AuthenticationDirective[AuthenticatedUser] - def authenticate(authCredentials: AuthCredentials): Future[Option[AuthenticatedUser]] + def authenticate(authCredentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] - def authenticationMethod(): EndpointInput[AuthCredentials] + def authenticationMethod(): EndpointInput[Credentials] final lazy val routeWithPathPrefix: Route = pathPrefix("authentication" / name.toLowerCase()) { @@ -63,76 +62,3 @@ object AuthenticationResources { } } - -trait AnonymousAccess extends Directives { - this: AuthenticationResources => - - protected def authenticateReally(): AuthenticationDirective[AuthenticatedUser] - - protected def rawAuthCredentialsMethod: EndpointInput[Option[String]] - - protected def authenticateReally(credentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] - - override final def authenticationMethod(): EndpointInput[AuthCredentials] = { - rawAuthCredentialsMethod.map(optionalStringToAuthCredentialsMapping(configuration.anonymousUserRole.isDefined)) - } - - override final def authenticate(authCredentials: AuthCredentials): Future[Option[AuthenticatedUser]] = { - authCredentials match { - case credentials @ AuthCredentials.PassedAuthCredentials(_) => - authenticateReally(credentials) - case AuthCredentials.AnonymousAccess => - Future.successful(Some(AuthenticatedUser.createAnonymousUser(configuration.anonymousUserRole.toSet))) - } - } - - override final def authenticate(): Directive1[AuthenticatedUser] = { - configuration.anonymousUserRole match { - case Some(role) => - authenticateOrPermitAnonymously(AuthenticatedUser.createAnonymousUser(Set(role))) - case None => - authenticateReally() - } - } - - private def authenticateOrPermitAnonymously( - anonymousUser: AuthenticatedUser - ): AuthenticationDirective[AuthenticatedUser] = { - def handleAuthorizationFailedRejection = handleRejections( - RejectionHandler - .newBuilder() - // If the authorization rejection was caused by anonymous access, - // we issue the Unauthorized status code with a challenge instead of the Forbidden - .handle { case AuthorizationFailedRejection => - authenticateReally() { _ => reject } - } - .result() - ) - - authenticateReally().optional.flatMap( - _.map(provide).getOrElse( - handleAuthorizationFailedRejection.tmap(_ => anonymousUser) - ) - ) - } - -} - -object AnonymousAccess { - - def optionalStringToAuthCredentialsMapping( - anonymousAccessEnabled: Boolean - ): Mapping[Option[String], AuthCredentials] = - Mapping - .fromDecode[Option[String], AuthCredentials] { - case Some(value) => DecodeResult.Value(AuthCredentials.PassedAuthCredentials(value)) - case None if anonymousAccessEnabled => - DecodeResult.Value(AuthCredentials.AnonymousAccess) - case None => - DecodeResult.Missing - } { - case AuthCredentials.PassedAuthCredentials(credentials) => Some(credentials) - case AuthCredentials.AnonymousAccess => None - } - -} diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationResources.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationResources.scala index 3a044460cdb..5fed89d20a9 100644 --- a/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationResources.scala +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationResources.scala @@ -14,8 +14,7 @@ class BasicAuthenticationResources( override val configuration: BasicAuthenticationConfiguration )( implicit executionContext: ExecutionContext -) extends AuthenticationResources - with AnonymousAccess { +) extends AuthenticationResources { override type CONFIG = BasicAuthenticationConfiguration @@ -23,18 +22,16 @@ class BasicAuthenticationResources( override protected val frontendStrategySettings: FrontendStrategySettings = FrontendStrategySettings.Browser - override protected def authenticateReally(): AuthenticationDirective[AuthenticatedUser] = + override def authenticate(): AuthenticationDirective[AuthenticatedUser] = SecurityDirectives.authenticateBasicAsync( authenticator = authenticator, realm = realm ) - override protected def authenticateReally(credentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] = { - authenticator.authenticate(credentials) + override def authenticate(authCredentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] = { + authenticator.authenticate(authCredentials) } - override protected def rawAuthCredentialsMethod: EndpointInput[Option[String]] = { + override def authenticationMethod(): EndpointInput[Credentials] = auth.basic[Option[String]](WWWAuthenticateChallenge.basic.realm(realm)) - } - } diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicHttpAuthenticator.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicHttpAuthenticator.scala index b6a3a967bd4..ce821346235 100644 --- a/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicHttpAuthenticator.scala +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/basicauth/BasicHttpAuthenticator.scala @@ -5,9 +5,8 @@ import akka.http.scaladsl.server.directives.Credentials.Provided import akka.http.scaladsl.server.directives.{Credentials, SecurityDirectives} import at.favre.lib.crypto.bcrypt.BCrypt import pl.touk.nussknacker.engine.util.cache.DefaultCache -import pl.touk.nussknacker.ui.security.api.AuthenticatedUser -import pl.touk.nussknacker.security.AuthCredentials import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials +import pl.touk.nussknacker.ui.security.api.AuthenticatedUser import pl.touk.nussknacker.ui.security.basicauth.BasicHttpAuthenticator.{ EncryptedPassword, PlainPassword, diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationResources.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationResources.scala index 1a9c122b494..d3a3e87cd56 100644 --- a/security/src/main/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationResources.scala +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationResources.scala @@ -5,13 +5,7 @@ import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsMissin import akka.http.scaladsl.server.directives.AuthenticationDirective import akka.http.scaladsl.server.{AuthenticationFailedRejection, Directive1} import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials -import pl.touk.nussknacker.ui.security.api.{ - AnonymousAccess, - AuthenticatedUser, - AuthenticationResources, - FrontendStrategySettings -} -import pl.touk.nussknacker.ui.security.basicauth.BasicAuthenticationConfiguration +import pl.touk.nussknacker.ui.security.api.{AuthenticatedUser, AuthenticationResources, FrontendStrategySettings} import sttp.model.headers.WWWAuthenticateChallenge import sttp.tapir._ @@ -21,24 +15,21 @@ import scala.concurrent.Future class DummyAuthenticationResources( override val name: String, override val configuration: DummyAuthenticationConfiguration -) extends AuthenticationResources - with AnonymousAccess { +) extends AuthenticationResources { override type CONFIG = DummyAuthenticationConfiguration override protected val frontendStrategySettings: FrontendStrategySettings = FrontendStrategySettings.Browser - override protected def authenticateReally(): AuthenticationDirective[AuthenticatedUser] = { + override def authenticate(): AuthenticationDirective[AuthenticatedUser] = reject(AuthenticationFailedRejection(CredentialsMissing, HttpChallenge("Dummy", "Dummy"))): Directive1[ AuthenticatedUser ] - } - override protected def authenticateReally(credentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] = { - Future.successful(None) - } + override def authenticate(authCredentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] = + Future.successful(Option.empty) - override protected def rawAuthCredentialsMethod: EndpointInput[Option[String]] = { + override def authenticationMethod(): EndpointInput[Credentials] = { auth.basic[Option[String]](new WWWAuthenticateChallenge("Dummy", ListMap.empty).realm("Dummy")) } diff --git a/security/src/main/scala/pl/touk/nussknacker/ui/security/oauth2/OAuth2AuthenticationResources.scala b/security/src/main/scala/pl/touk/nussknacker/ui/security/oauth2/OAuth2AuthenticationResources.scala index fac2660bbac..bd10dca0aae 100644 --- a/security/src/main/scala/pl/touk/nussknacker/ui/security/oauth2/OAuth2AuthenticationResources.scala +++ b/security/src/main/scala/pl/touk/nussknacker/ui/security/oauth2/OAuth2AuthenticationResources.scala @@ -29,8 +29,7 @@ class OAuth2AuthenticationResources( )(implicit executionContext: ExecutionContext, sttpBackend: SttpBackend[Future, Any]) extends AuthenticationResources with Directives - with LazyLogging - with AnonymousAccess { + with LazyLogging { import pl.touk.nussknacker.engine.util.Implicits.RichIterable @@ -38,25 +37,23 @@ class OAuth2AuthenticationResources( private val authenticator = OAuth2Authenticator(service) - override protected def authenticateReally(): AuthenticationDirective[AuthenticatedUser] = { + override def authenticate(): AuthenticationDirective[AuthenticatedUser] = SecurityDirectives.authenticateOAuth2Async( authenticator = authenticator, realm = realm ) - } - override protected def authenticateReally(credentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] = { - authenticator.authenticate(credentials.value) + override def authenticate(authCredentials: PassedAuthCredentials): Future[Option[AuthenticatedUser]] = { + authenticator.authenticate(authCredentials.value) } - override protected def rawAuthCredentialsMethod: EndpointInput[Option[String]] = { + override def authenticationMethod(): EndpointInput[Credentials] = optionalOauth2AuthorizationCode( authorizationUrl = configuration.authorizeUrl.map(_.toString), // it's only for OpenAPI UI purpose to be able to use "Try It Out" feature. UI calls authorization URL // (e.g. Github) and then calls our proxy for Bearer token. It uses the received token while calling the NU API tokenUrl = Some(s"../authentication/${name.toLowerCase()}"), ) - } override protected val frontendStrategySettings: FrontendStrategySettings = configuration.overrideFrontendAuthenticationStrategy.getOrElse( diff --git a/security/src/test/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManagerSpec.scala b/security/src/test/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManagerSpec.scala new file mode 100644 index 00000000000..669368f9b85 --- /dev/null +++ b/security/src/test/scala/pl/touk/nussknacker/ui/security/api/AuthenticationManagerSpec.scala @@ -0,0 +1,129 @@ +package pl.touk.nussknacker.ui.security.api + +import akka.http.javadsl.model.headers.HttpCredentials +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.headers.RawHeader +import akka.http.scaladsl.server.{Directives, Route} +import akka.http.scaladsl.testkit.ScalatestRouteTest +import com.typesafe.config.ConfigFactory +import io.circe.syntax._ +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.security.ImpersonatedUserData +import pl.touk.nussknacker.ui.security.accesslogic.ImpersonatedAccess +import pl.touk.nussknacker.ui.security.basicauth.BasicAuthenticationResources +import sttp.client3.testing.SttpBackendStub + +import scala.concurrent.Future + +class AuthenticationManagerSpec extends AnyFunSpec with Matchers with ScalatestRouteTest with Directives { + + implicit private val testingBackend: SttpBackendStub[Future, Any] = SttpBackendStub.asynchronousFuture + private val classLoader = getClass.getClassLoader + + private val username = "admin" + private val anonymousUsername = "anonymous" + private val impersonatedUsername = "user" + + private val config = ConfigFactory.parseString(s""" + authentication: { + method: "BasicAuth" + anonymousUserRole: "Anonymous" + usersFile: "classpath:basic-users.conf" + } + """.stripMargin) + + private val authenticationResources = AuthenticationResources(config, classLoader, testingBackend) + assert(authenticationResources.isInstanceOf[BasicAuthenticationResources]) + private val authenticationManager = new AuthenticationManager(authenticationResources) + + private val testRoute = Route.seal( + authenticationManager.authenticate() { authenticatedUser => + val endAuthenticatedUser = authenticatedUser.impersonatedAuthenticationUser match { + case Some(impersonatedUser) => impersonatedUser + case None => authenticatedUser + } + path("public") { + get { + complete(endAuthenticatedUser.username) + } + } ~ path("processes") { + authorize(endAuthenticatedUser.roles.contains("Admin")) { + get { + complete(endAuthenticatedUser.username) + } + } + } + } + ) + + it("should authenticate an anonymous user when requesting an unrestricted resource") { + Get("/public") ~> testRoute ~> check { + status shouldEqual StatusCodes.OK + responseAs[String] shouldEqual s"$anonymousUsername" + } + } + + it("should not authenticate an anonymous user when requesting a restricted resource") { + Get("/processes") ~> testRoute ~> check { + status shouldEqual StatusCodes.Unauthorized + } + } + + it("should authenticate user with passed credentials") { + Get("/processes").addCredentials( + HttpCredentials.createBasicHttpCredentials(username, username) + ) ~> testRoute ~> check { + status shouldEqual StatusCodes.OK + responseAs[String] shouldEqual s"$username" + } + } + + it("should not authenticate user with invalid credentials") { + Get("/processes").addCredentials( + HttpCredentials.createBasicHttpCredentials(username, "wrong") + ) ~> testRoute ~> check { + status shouldEqual StatusCodes.Unauthorized + } + } + + it("should impersonate when provided with credentials and the header") { + Get("/public") + .addCredentials(HttpCredentials.createBasicHttpCredentials(username, username)) + .addHeader( + RawHeader( + ImpersonatedAccess.impersonateHeaderName, + ImpersonatedUserData(impersonatedUsername, impersonatedUsername, Set("User")).asJson.noSpaces + ) + ) ~> testRoute ~> check { + status shouldEqual StatusCodes.OK + responseAs[String] shouldEqual s"$impersonatedUsername" + } + } + + it("should not impersonate when provided with invalid credentials and the header") { + Get("/public") + .addCredentials(HttpCredentials.createBasicHttpCredentials(username, "wrong")) + .addHeader( + RawHeader( + ImpersonatedAccess.impersonateHeaderName, + ImpersonatedUserData(impersonatedUsername, impersonatedUsername, Set("User")).asJson.noSpaces + ) + ) ~> testRoute ~> check { + status shouldEqual StatusCodes.Unauthorized + } + } + + it("should not authenticate an anonymous user when requesting a restricted resource with impersonation") { + Get("/processes") + .addHeader( + RawHeader( + ImpersonatedAccess.impersonateHeaderName, + ImpersonatedUserData(impersonatedUsername, impersonatedUsername, Set("User")).asJson.noSpaces + ) + ) ~> testRoute ~> check { + status shouldEqual StatusCodes.Unauthorized + } + } + +} diff --git a/security/src/test/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationSpec.scala b/security/src/test/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationSpec.scala index ea642b3a68d..5c8a1d05b01 100644 --- a/security/src/test/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationSpec.scala +++ b/security/src/test/scala/pl/touk/nussknacker/ui/security/basicauth/BasicAuthenticationSpec.scala @@ -46,32 +46,12 @@ class BasicAuthenticationSpec extends AnyFunSpec with Matchers with ScalatestRou } ) - it("should permit an anonymous user") { - Get("/public") ~> testRoute ~> check { - status shouldEqual StatusCodes.OK - responseAs[String] shouldEqual s"${anonymousUserRole}" - } - } - - it("should permit an authenticated user") { - Get("/public").addCredentials(HttpCredentials.createBasicHttpCredentials("user", "user")) ~> testRoute ~> check { - status shouldEqual StatusCodes.OK - responseAs[String] shouldEqual "User" - } - } - it("should request authorization on invalid credentials") { Get("/public").addCredentials(HttpCredentials.createBasicHttpCredentials("user", "invalid")) ~> testRoute ~> check { status shouldEqual StatusCodes.Unauthorized } } - it("should request authorization on anonymous access to a restricted resource") { - Get("/config") ~> testRoute ~> check { - status shouldEqual StatusCodes.Unauthorized - } - } - it("should permit an authorized user to a restricted resource") { Get("/config").addCredentials(HttpCredentials.createBasicHttpCredentials("admin", "admin")) ~> testRoute ~> check { status shouldEqual StatusCodes.OK diff --git a/security/src/test/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationSpec.scala b/security/src/test/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationSpec.scala deleted file mode 100644 index 48f1f43a744..00000000000 --- a/security/src/test/scala/pl/touk/nussknacker/ui/security/dummy/DummyAuthenticationSpec.scala +++ /dev/null @@ -1,43 +0,0 @@ -package pl.touk.nussknacker.ui.security.dummy - -import akka.http.scaladsl.server.Directives -import akka.http.scaladsl.testkit.ScalatestRouteTest -import com.typesafe.config.ConfigFactory -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import pl.touk.nussknacker.ui.security.api.AuthenticationResources -import sttp.client3.testing.SttpBackendStub - -import scala.concurrent.Future - -class DummyAuthenticationSpec extends AnyFunSpec with Matchers with ScalatestRouteTest with Directives { - - implicit private val testingBackend: SttpBackendStub[Future, Any] = SttpBackendStub.asynchronousFuture - private val classLoader = getClass.getClassLoader - - it("should authenticate an anonymous user") { - - val anonymousUserRole = "Test" - val config = ConfigFactory.parseString(s""" - authentication: { - method: "Dummy" - anonymousUserRole: "${anonymousUserRole}" - } - """.stripMargin) - - val authenticationResources = AuthenticationResources(config, classLoader, testingBackend) - assert(authenticationResources.isInstanceOf[DummyAuthenticationResources]) - - val testRoute = - authenticationResources.authenticate() { authenticatedUser => - get { - complete(authenticatedUser.roles.mkString) - } - } - - Get() ~> testRoute ~> check { - responseAs[String] shouldEqual s"${anonymousUserRole}" - } - } - -}