Skip to content

Commit

Permalink
Ability to configure a required permission for component links (#7388)
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszkp96 authored Jan 7, 2025
1 parent 7901733 commit 223f484
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import pl.touk.nussknacker.engine.api.component.DesignerWideComponentId
import pl.touk.nussknacker.engine.api.component.ComponentType.ComponentType
import pl.touk.nussknacker.engine.util.UriUtils
import pl.touk.nussknacker.restmodel.component.ComponentLink
import pl.touk.nussknacker.ui.security.api.GlobalPermission.GlobalPermission
import pl.touk.nussknacker.ui.security.api.{AdminUser, CommonUser, ImpersonatedUser, LoggedUser, RealLoggedUser}

import java.net.URI

Expand All @@ -15,12 +17,13 @@ final case class ComponentLinkConfig(
icon: URI,
url: URI,
// FIXME: It should be probably supportedComponentIds - currently this filtering is unusable
supportedComponentTypes: Option[List[ComponentType]]
supportedComponentTypes: Option[List[ComponentType]],
requiredPermission: Option[GlobalPermission],
) {
import ComponentLinkConfig._

def isAvailable(componentType: ComponentType): Boolean =
supportedComponentTypes.isEmpty || supportedComponentTypes.exists(_.contains(componentType))
def isAvailable(componentType: ComponentType, loggedUser: LoggedUser): Boolean =
isSupportedComponentType(componentType) && isPermitted(loggedUser)

def toComponentLink(designerWideComponentId: DesignerWideComponentId, componentName: String): ComponentLink =
ComponentLink(
Expand All @@ -30,6 +33,16 @@ final case class ComponentLinkConfig(
URI.create(fillByComponentData(url.toString, designerWideComponentId, componentName, urlOption = true))
)

private def isSupportedComponentType(componentType: ComponentType) = {
supportedComponentTypes.isEmpty || supportedComponentTypes.exists(
_.contains(componentType)
)
}

private def isPermitted(loggedUser: LoggedUser) = {
requiredPermission.isEmpty || requiredPermission.exists(loggedUser.hasPermission)
}

}

object ComponentLinkConfig {
Expand All @@ -49,6 +62,18 @@ object ComponentLinkConfig {
.replace(ComponentNameTemplate, name)
}

implicit class LoggedUserOps(val loggedUser: LoggedUser) extends AnyVal {

def hasPermission(permission: GlobalPermission): Boolean = loggedUser match {
case CommonUser(_, _, _, globalPermissions) =>
globalPermissions.contains(permission)
case _: AdminUser => true
case ImpersonatedUser(impersonatedUser, _) =>
impersonatedUser.hasPermission(permission)
}

}

}

object ComponentLinksConfigExtractor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class DefaultComponentService(
private def createComponents(
componentsDefinition: List[ComponentDefinitionWithImplementation],
category: String,
): List[ComponentListElement] = {
)(implicit loggedUser: LoggedUser): List[ComponentListElement] = {
componentsDefinition
.map { definition =>
val designerWideId = definition.designerWideId
Expand Down Expand Up @@ -212,9 +212,9 @@ class DefaultComponentService(
private def createComponentLinks(
designerWideId: DesignerWideComponentId,
component: ComponentDefinitionWithImplementation
): List[ComponentLink] = {
)(implicit loggedUser: LoggedUser): List[ComponentLink] = {
val componentLinks = componentLinksConfig
.filter(_.isAvailable(component.componentType))
.filter(_.isAvailable(component.componentType, loggedUser))
.map(_.toComponentLink(designerWideId, component.name))

// If component configuration contains documentation link then we add base link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLo
import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider
import pl.touk.nussknacker.ui.process.processingtype.{ProcessingTypeData, ScenarioParametersService}
import pl.touk.nussknacker.ui.process.repository.ScenarioWithDetailsEntity
import pl.touk.nussknacker.ui.security.api.{LoggedUser, RealLoggedUser}
import pl.touk.nussknacker.ui.security.api.GlobalPermission.GlobalPermission
import pl.touk.nussknacker.ui.security.api.{AdminUser, CommonUser, ImpersonatedUser, LoggedUser, RealLoggedUser}

import java.net.URI
import scala.annotation.tailrec

class DefaultComponentServiceSpec
extends AnyFlatSpec
Expand All @@ -73,34 +75,40 @@ class DefaultComponentServiceSpec
private val editLinkId = "edit"
private val filterLinkId = "filter"

private val invokePermission: GlobalPermission = "InvokePermissionExample"

private val linkConfigs = List(
createLinkConfig(
usagesLinkId,
s"Usages of $ComponentNameTemplate",
s"/assets/components/links/usages.svg",
s"https://list-of-usages.com/$ComponentIdTemplate/",
None,
None
),
createLinkConfig(
invokeLinkId,
s"Invoke component $ComponentNameTemplate",
s"/assets/components/links/invoke.svg",
s"https://components.com/$ComponentIdTemplate/Invoke",
Some(List(Service))
Some(List(Service)),
Some(invokePermission),
),
createLinkConfig(
editLinkId,
s"Edit component $ComponentNameTemplate",
"/assets/components/links/edit.svg",
s"https://components.com/$ComponentIdTemplate/",
Some(List(CustomComponent, Service))
Some(List(CustomComponent, Service)),
None,
),
createLinkConfig(
filterLinkId,
s"Custom link $ComponentNameTemplate",
"https://other-domain.com/assets/components/links/filter.svg",
s"https://components.com/$ComponentIdTemplate/filter",
Some(List(BuiltIn))
Some(List(BuiltIn)),
None,
),
)

Expand All @@ -113,13 +121,14 @@ class DefaultComponentServiceSpec
${linkConfigs
.map { link =>
s"""{
| id: "${link.id}",
| title: "${link.title}",
| url: "${link.url}",
| icon: "${link.icon}",
| id: "${link.id}"
| title: "${link.title}"
| url: "${link.url}"
| icon: "${link.icon}"
| ${link.supportedComponentTypes
.map(types => s"""supportedComponentTypes: [${types.mkString(",")}]""")
.getOrElse("")}
| ${link.requiredPermission.map(permission => s"""requiredPermission: "$permission"""").getOrElse("")}
| }""".stripMargin
}
.mkString(",\n")}
Expand Down Expand Up @@ -224,7 +233,7 @@ class DefaultComponentServiceSpec
|}
|""".stripMargin)

private val baseComponents: List[ComponentListElement] =
private def baseComponents(implicit user: LoggedUser): List[ComponentListElement] =
List(
baseComponent(BuiltInComponentId.Filter, overriddenIcon, BaseGroupName, AllCategories),
baseComponent(BuiltInComponentId.Split, SplitIcon, BaseGroupName, AllCategories),
Expand Down Expand Up @@ -360,7 +369,7 @@ class DefaultComponentServiceSpec
)
}

private val fragmentMarketingComponents: List[ComponentListElement] = {
private def fragmentMarketingComponents(implicit loggedUser: LoggedUser): List[ComponentListElement] = {
val cat = CategoryMarketing
val componentId = ComponentId(Fragment, cat)
val designerWideComponentId = cid(ProcessingTypeStreaming, componentId)
Expand All @@ -381,7 +390,7 @@ class DefaultComponentServiceSpec
)
}

private val fragmentFraudComponents: List[ComponentListElement] = {
private def fragmentFraudComponents(implicit loggedUser: LoggedUser): List[ComponentListElement] = {
val cat = CategoryFraud
val componentId = ComponentId(Fragment, cat)
val designerWideComponentId = cid(ProcessingTypeFraud, componentId)
Expand Down Expand Up @@ -469,7 +478,7 @@ class DefaultComponentServiceSpec
icon: String,
componentGroupName: ComponentGroupName,
categories: List[String]
): ComponentListElement = {
)(implicit loggedUser: LoggedUser): ComponentListElement = {
val designerWideComponentId = bid(componentId)
val docsLinks = if (componentId.name == BuiltInComponentId.Filter.name) List(filterDocsLink) else Nil
val links = docsLinks ++ createLinks(designerWideComponentId, componentId)
Expand All @@ -489,9 +498,9 @@ class DefaultComponentServiceSpec
private def createLinks(
determineDesignerWideId: DesignerWideComponentId,
componentId: ComponentId
): List[ComponentLink] =
)(implicit loggedUser: LoggedUser): List[ComponentLink] =
linkConfigs
.filter(_.isAvailable(componentId.`type`))
.filter(_.isAvailable(componentId.`type`, loggedUser))
.map(_.toComponentLink(determineDesignerWideId, componentId.name))

private def componentCount(determineDesignerWideId: DesignerWideComponentId, user: LoggedUser) = {
Expand Down Expand Up @@ -527,7 +536,8 @@ class DefaultComponentServiceSpec
private val fraudUser = RealLoggedUser(
id = "1",
username = "fraudUser",
categoryPermissions = Map(CategoryFraud -> Set(Permission.Read))
categoryPermissions = Map(CategoryFraud -> Set(Permission.Read)),
globalPermissions = List(invokePermission)
)

private val providerComponents =
Expand Down Expand Up @@ -561,9 +571,10 @@ class DefaultComponentServiceSpec

def filterUserComponents(user: LoggedUser, categories: List[String]): List[ComponentListElement] =
prepareComponents(user)
.map(c => c -> categories.intersect(c.categories))
.filter(seq => seq._2.nonEmpty)
.map(seq => seq._1.copy(categories = seq._2))
.collect {
case component if categories.intersect(component.categories).nonEmpty =>
component.copy(categories = categories)
}

private val expectedAdminComponents = prepareComponents(admin)
private val expectedMarketingComponents = filterUserComponents(marketingUser, List(CategoryMarketing))
Expand Down Expand Up @@ -625,7 +636,7 @@ class DefaultComponentServiceSpec
returnedCounts should contain theSameElementsAs expectedCounts

forAll(Table("returnedComponents", returnedComponentsWithUsages: _*)) { returnedComponent =>
checkLinks(returnedComponent)
checkLinks(returnedComponent, user)

// Components should contain only user categories
(returnedComponent.categories diff possibleCategories) shouldBe empty
Expand All @@ -636,10 +647,11 @@ class DefaultComponentServiceSpec
}
}

private def checkLinks(returnedComponent: ComponentListElement): Unit = {
private def checkLinks(returnedComponent: ComponentListElement, loggedUser: LoggedUser): Unit = {
// See linksConfig
val availableLinksId = returnedComponent.componentId match {
case ComponentId(Service, _) => List(usagesLinkId, invokeLinkId, editLinkId)
case ComponentId(Service, _) =>
List(usagesLinkId, editLinkId) ++ List(invokeLinkId).filter(_ => hasPermission(loggedUser, invokePermission))
case ComponentId(CustomComponent, _) => List(usagesLinkId, editLinkId)
case ComponentId(BuiltIn, _) => List(usagesLinkId, filterLinkId)
case _ => List(usagesLinkId)
Expand Down Expand Up @@ -891,8 +903,19 @@ class DefaultComponentServiceSpec
title: String,
icon: String,
url: String,
supportedComponentTypes: Option[List[ComponentType]]
supportedComponentTypes: Option[List[ComponentType]],
requiredPermission: Option[GlobalPermission],
): ComponentLinkConfig =
ComponentLinkConfig(id, title, URI.create(icon), URI.create(url), supportedComponentTypes)
ComponentLinkConfig(id, title, URI.create(icon), URI.create(url), supportedComponentTypes, requiredPermission)

@tailrec
private def hasPermission(loggedUser: LoggedUser, permission: GlobalPermission): Boolean = loggedUser match {
case user: RealLoggedUser =>
user match {
case CommonUser(_, _, _, globalPermissions) => globalPermissions.contains(permission)
case _: AdminUser => true
}
case ImpersonatedUser(impersonatedUser, _) => hasPermission(impersonatedUser, permission)
}

}
1 change: 1 addition & 0 deletions docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
generated and emitted 5 times. Now in one count batch each value is evaluated separately.
* [#7386](https://github.com/TouK/nussknacker/pull/7386) Improve Periodic DeploymentManager db queries, continuation of [#7323](https://github.com/TouK/nussknacker/pull/7323)
* [#7360](https://github.com/TouK/nussknacker/pull/7360) Added Median aggregator
* [#7388](https://github.com/TouK/nussknacker/pull/7388) Ability to configure a required permission for component links

## 1.18

Expand Down
3 changes: 2 additions & 1 deletion docs/configuration/model/ModelConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,11 @@ componentLinks: [
icon: "/assets/components/CustomNode.svg"
url: "https://myCustom.com/dataSource/$componentName"
supportedComponentTypes: ["service"]
requiredPermission: "ServicePermission"
}
]
```
Fields `title`, `icon`, `url` can contain templates: `$componentId` nad `$componentName` which are replaced by component data. Param `supportedComponentTypes` means component's types which can support links.
Fields `title`, `icon`, `url` can contain templates: `$componentId` nad `$componentName` which are replaced by component data. Param `supportedComponentTypes` means component's types which can support links. Optional param `requiredPermission` allows link access to users with a given permission.

### Component group mapping

Expand Down

0 comments on commit 223f484

Please sign in to comment.