diff --git a/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala b/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala index 03c0f6cb851..616d456434e 100644 --- a/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala +++ b/designer/restmodel/src/main/scala/pl/touk/nussknacker/restmodel/definition/package.scala @@ -4,7 +4,7 @@ import io.circe.generic.JsonCodec import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder} import io.circe.{Decoder, Encoder} import pl.touk.nussknacker.engine.api.component.{ComponentGroupName, ComponentId} -import pl.touk.nussknacker.engine.api.definition.ParameterEditor +import pl.touk.nussknacker.engine.api.definition.{ParameterEditor, RawParameterEditor} import pl.touk.nussknacker.engine.api.typed.typing.TypingResult import pl.touk.nussknacker.engine.graph.EdgeType import pl.touk.nussknacker.engine.graph.evaluatedparam.{Parameter => NodeParameter} @@ -140,6 +140,10 @@ package object definition { hintText: Option[String] ) + object UiActionParameterConfig { + def empty: UiActionParameterConfig = UiActionParameterConfig(None, RawParameterEditor, None, None) + } + object UIParameter { implicit def decoder(implicit typing: Decoder[TypingResult]): Decoder[UIParameter] = deriveConfiguredDecoder[UIParameter] diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ActionInfoHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ActionInfoHttpService.scala new file mode 100644 index 00000000000..f22c7ff2332 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ActionInfoHttpService.scala @@ -0,0 +1,71 @@ +package pl.touk.nussknacker.ui.api + +import com.typesafe.scalalogging.LazyLogging +import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.ui.api.ActionInfoHttpService.ActionInfoError +import pl.touk.nussknacker.ui.api.ActionInfoHttpService.ActionInfoError.{NoPermission, NoScenario} +import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError +import pl.touk.nussknacker.ui.api.description.ActionInfoEndpoints +import pl.touk.nussknacker.ui.api.utils.ScenarioHttpServiceExtensions +import pl.touk.nussknacker.ui.process.ProcessService +import pl.touk.nussknacker.ui.process.newactivity.ActionInfoService +import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider +import pl.touk.nussknacker.ui.security.api.AuthManager +import sttp.tapir.{Codec, CodecFormat} + +import scala.concurrent.ExecutionContext + +class ActionInfoHttpService( + authManager: AuthManager, + processingTypeToActionInfoService: ProcessingTypeDataProvider[ActionInfoService, _], + protected override val scenarioService: ProcessService +)(implicit val executionContext: ExecutionContext) + extends BaseHttpService(authManager) + with ScenarioHttpServiceExtensions + with LazyLogging { + + override protected type BusinessErrorType = ActionInfoError + override protected def noScenarioError(scenarioName: ProcessName): ActionInfoError = NoScenario(scenarioName) + override protected def noPermissionError: ActionInfoError with CustomAuthorizationError = NoPermission + + private val securityInput = authManager.authenticationEndpointInput() + + private val endpoints = new ActionInfoEndpoints(securityInput) + + expose { + endpoints.actionParametersEndpoint + .serverSecurityLogic(authorizeKnownUser[ActionInfoError]) + .serverLogicEitherT { implicit loggedUser => actionParametersInput => + val (scenarioName, scenarioGraph) = actionParametersInput + for { + scenarioWithDetails <- getScenarioWithDetailsByName(scenarioName) + actionInfoService = processingTypeToActionInfoService.forProcessingTypeUnsafe( + scenarioWithDetails.processingType + ) + actionParameters = actionInfoService.getActionParameters( + scenarioGraph, + scenarioWithDetails.processVersionUnsafe, + scenarioWithDetails.isFragment + ) + } yield actionParameters + } + } + +} + +object ActionInfoHttpService { + + sealed trait ActionInfoError + + object ActionInfoError { + final case class NoScenario(scenarioName: ProcessName) extends ActionInfoError + final case object NoPermission extends ActionInfoError with CustomAuthorizationError + + implicit val noScenarioCodec: Codec[String, NoScenario, CodecFormat.TextPlain] = { + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoScenario](e => s"No scenario ${e.scenarioName} found") + } + + } + +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ActionInfoResources.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ActionInfoResources.scala deleted file mode 100644 index 3928872ac1a..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ActionInfoResources.scala +++ /dev/null @@ -1,45 +0,0 @@ -package pl.touk.nussknacker.ui.api - -import akka.http.scaladsl.server.{Directives, Route} -import com.typesafe.scalalogging.LazyLogging -import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport -import pl.touk.nussknacker.engine.api.graph.ScenarioGraph -import pl.touk.nussknacker.ui.api.utils.ScenarioDetailsOps.ScenarioWithDetailsOps -import pl.touk.nussknacker.ui.process.ProcessService -import pl.touk.nussknacker.ui.process.newactivity.ActionInfoService -import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider -import pl.touk.nussknacker.ui.security.api.LoggedUser - -import scala.concurrent.ExecutionContext - -class ActionInfoResources( - protected val processService: ProcessService, - actionInfoService: ProcessingTypeDataProvider[ActionInfoService, _] -)(implicit val ec: ExecutionContext) - extends Directives - with FailFastCirceSupport - with RouteWithUser - with ProcessDirectives - with LazyLogging { - - def securedRoute(implicit user: LoggedUser): Route = { - pathPrefix("actionInfo" / ProcessNameSegment) { processName => - (post & processDetailsForName(processName)) { processDetails => - entity(as[ScenarioGraph]) { scenarioGraph => - path("actionParameters") { - complete { - actionInfoService - .forProcessingTypeUnsafe(processDetails.processingType) - .getActionParameters( - scenarioGraph, - processDetails.processVersionUnsafe, - processDetails.isFragment - ) - } - } - } - } - } - } - -} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ActionInfoEndpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ActionInfoEndpoints.scala new file mode 100644 index 00000000000..3fcb291afac --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ActionInfoEndpoints.scala @@ -0,0 +1,93 @@ +package pl.touk.nussknacker.ui.api.description + +import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName +import pl.touk.nussknacker.engine.api.{NodeId, StreamMetaData} +import pl.touk.nussknacker.engine.api.graph.{ProcessProperties, ScenarioGraph} +import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint +import pl.touk.nussknacker.restmodel.definition.UiActionParameterConfig +import pl.touk.nussknacker.security.AuthCredentials +import pl.touk.nussknacker.ui.api.ActionInfoHttpService.ActionInfoError +import pl.touk.nussknacker.ui.api.ActionInfoHttpService.ActionInfoError.NoScenario +import pl.touk.nussknacker.ui.api.TapirCodecs.ScenarioNameCodec._ +import pl.touk.nussknacker.ui.api.TapirCodecs.ScenarioGraphCodec._ +import pl.touk.nussknacker.ui.api.description.ActionInfoEndpoints.Examples.noScenarioExample +import pl.touk.nussknacker.ui.api.description.ActionInfoEndpoints._ +import pl.touk.nussknacker.ui.process.newactivity.ActionInfoService.{UiActionNodeParameters, UiActionParameters} +import sttp.model.StatusCode.{NotFound, Ok} +import sttp.tapir.EndpointIO.Example +import sttp.tapir._ +import sttp.tapir.json.circe.jsonBody + +class ActionInfoEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpointDefinitions { + + lazy val actionParametersEndpoint + : SecuredEndpoint[(ProcessName, ScenarioGraph), ActionInfoError, UiActionParameters, Any] = + baseNuApiEndpoint + .summary("Get action parameters") + .tag("Deployments") + .post + .in("actionInfo" / path[ProcessName]("scenarioName") / "actionParameters") + .in( + jsonBody[ScenarioGraph] + .example(simpleGraphExample) + ) + .out( + statusCode(Ok).and( + jsonBody[UiActionParameters] + .examples( + List( + Example.of( + summary = Some("Valid action parameters for given scenario"), + value = Map( + ScenarioActionName.Deploy -> List( + UiActionNodeParameters( + NodeId("sample node id"), + Map("param name" -> UiActionParameterConfig.empty) + ) + ) + ) + ) + ) + ) + ) + ) + .errorOut( + oneOf[ActionInfoError]( + noScenarioExample + ) + ) + .withSecurity(auth) + + private val simpleGraphExample: Example[ScenarioGraph] = Example.of( + ScenarioGraph( + ProcessProperties(StreamMetaData()), + List(), + List(), + ) + ) + +} + +object ActionInfoEndpoints { + + implicit val uiActionParametersSchema: Schema[UiActionParameters] = Schema.anyObject + + object Examples { + + val noScenarioExample: EndpointOutput.OneOfVariant[NoScenario] = + oneOfVariantFromMatchType( + NotFound, + plainBody[NoScenario] + .example( + Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + ) + ) + + } + +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActionInfoService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActionInfoService.scala index cd7a7683a57..2cb6ec7e6d0 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActionInfoService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActionInfoService.scala @@ -9,7 +9,7 @@ import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.definition.activity.ActionInfoProvider import pl.touk.nussknacker.restmodel.definition.UiActionParameterConfig -import pl.touk.nussknacker.ui.process.newactivity.ActionInfoService.UiActionNodeParameters +import pl.touk.nussknacker.ui.process.newactivity.ActionInfoService.{UiActionNodeParameters, UiActionParameters} import pl.touk.nussknacker.ui.security.api.LoggedUser import pl.touk.nussknacker.ui.uiresolving.UIProcessResolver @@ -21,12 +21,12 @@ class ActionInfoService(activityInfoProvider: ActionInfoProvider, processResolve isFragment: Boolean )( implicit user: LoggedUser - ): Map[String, List[UiActionNodeParameters]] = { + ): UiActionParameters = { val canonical = toCanonicalProcess(scenarioGraph, processVersion, isFragment) activityInfoProvider .getActionParameters(processVersion, canonical) .map { case (scenarioActionName, nodeParamsMap) => - scenarioActionName.value -> nodeParamsMap.map { case (nodeId, params) => + scenarioActionName -> nodeParamsMap.map { case (nodeId, params) => UiActionNodeParameters( nodeId, params.map { case (name, value) => @@ -55,4 +55,5 @@ class ActionInfoService(activityInfoProvider: ActionInfoProvider, processResolve object ActionInfoService { @JsonCodec case class UiActionNodeParameters(nodeId: NodeId, parameters: Map[String, UiActionParameterConfig]) + type UiActionParameters = Map[ScenarioActionName, List[UiActionNodeParameters]] } 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 70d95accf73..6248b5f9fd0 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 @@ -222,7 +222,7 @@ class AkkaHttpBasedRouteProvider( new ScenarioTestExecutorServiceImpl(scenarioResolver, deploymentManager) ) } - val scenarioActivityService = scenarioTestServiceDeps.mapValues { case (_, processResolver, _, modelData, _) => + val actionInfoService = scenarioTestServiceDeps.mapValues { case (_, processResolver, _, modelData, _) => new ActionInfoService( new ModelDataActionInfoProvider(modelData), processResolver @@ -418,6 +418,12 @@ class AkkaHttpBasedRouteProvider( scenarioService = processService, ) + val actionInfoHttpService = new ActionInfoHttpService( + authManager = authManager, + processingTypeToActionInfoService = actionInfoService, + scenarioService = processService, + ) + val stickyNotesApiHttpService = new StickyNotesApiHttpService( authManager = authManager, stickyNotesRepository = stickyNotesRepository, @@ -528,7 +534,6 @@ class AkkaHttpBasedRouteProvider( ) } ), - new ActionInfoResources(processService, scenarioActivityService), new StatusResources(stateDefinitionService), ) @@ -616,6 +621,7 @@ class AkkaHttpBasedRouteProvider( migrationApiHttpService, nodesApiHttpService, testingApiHttpService, + actionInfoHttpService, notificationApiHttpService, scenarioActivityApiHttpService, scenarioLabelsApiHttpService, diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index a42a7888861..14cd4dd87bc 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -221,6 +221,114 @@ paths: security: - {} - httpAuth: [] + /api/actionInfo/{scenarioName}/actionParameters: + post: + tags: + - Deployments + summary: Get action parameters + operationId: postApiActioninfoScenarionameActionparameters + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + example: + properties: + additionalFields: + properties: + parallelism: '' + spillStateToDisk: 'true' + useAsyncInterpretation: '' + checkpointIntervalInSeconds: '' + metaDataType: StreamMetaData + showDescription: false + nodes: [] + edges: [] + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + examples: + Example: + summary: Valid action parameters for given scenario + value: + DEPLOY: + - nodeId: sample node id + parameters: + param name: + editor: + type: RawParameterEditor + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + CannotAuthenticateUser: + value: The supplied authentication is invalid + ImpersonatedUserNotExistsError: + value: No impersonated user data found for provided identity + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + InsufficientPermission: + value: The supplied authentication is not authorized to access this + resource + ImpersonationMissingPermission: + value: The supplied authentication is not authorized to impersonate + '404': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] /api/app/healthCheck: get: tags: diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala index de5d1dc2bec..3551c116d5e 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala @@ -2,7 +2,7 @@ package pl.touk.nussknacker.engine.api.deployment import io.circe.generic.JsonCodec import io.circe.generic.extras.semiauto.{deriveUnwrappedDecoder, deriveUnwrappedEncoder} -import io.circe.{Decoder, Encoder} +import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import pl.touk.nussknacker.engine.api.component.ParameterConfig import pl.touk.nussknacker.engine.api.deployment.ProcessActionState.ProcessActionState import pl.touk.nussknacker.engine.api.parameter.ParameterName @@ -73,6 +73,9 @@ object ScenarioActionName { implicit val encoder: Encoder[ScenarioActionName] = deriveUnwrappedEncoder implicit val decoder: Decoder[ScenarioActionName] = deriveUnwrappedDecoder + implicit val keyEncoder: KeyEncoder[ScenarioActionName] = KeyEncoder.encodeKeyString.contramap(_.value) + implicit val keyDecoder: KeyDecoder[ScenarioActionName] = KeyDecoder.decodeKeyString.map(ScenarioActionName(_)) + val Deploy: ScenarioActionName = ScenarioActionName("DEPLOY") val Cancel: ScenarioActionName = ScenarioActionName("CANCEL") val Archive: ScenarioActionName = ScenarioActionName("ARCHIVE")