Skip to content

Commit

Permalink
Fixes #22601: Missing validation button with admin
Browse files Browse the repository at this point in the history
  • Loading branch information
fanf committed Apr 20, 2023
1 parent d78cc48 commit 3103cf4
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import com.normation.box._
import com.normation.errors._
import com.normation.eventlog.EventActor
import com.normation.eventlog.ModificationId
import com.normation.rudder.AuthorizationType
import com.normation.rudder.Role
import com.normation.rudder.batch.AsyncWorkflowInfo
import com.normation.rudder.domain.eventlog.AddChangeRequestDiff
import com.normation.rudder.domain.eventlog.ChangeRequestDiff
Expand All @@ -54,6 +56,7 @@ import com.normation.rudder.services.workflows.NoWorkflowAction
import com.normation.rudder.services.workflows.WorkflowAction
import com.normation.rudder.services.workflows.WorkflowService
import com.normation.rudder.services.workflows.WorkflowUpdate
import com.normation.rudder.web.services.CurrentUser
import com.normation.utils.StringUuidGenerator
import net.liftweb.common._
import zio._
Expand Down Expand Up @@ -99,22 +102,39 @@ class EitherWorkflowService(cond: () => Box[Boolean], whenTrue: WorkflowService,

object TwoValidationStepsWorkflowServiceImpl {
case object Validation extends WorkflowNode {
val id = WorkflowNodeId("Pending validation")
val id: WorkflowNodeId = WorkflowNodeId("Pending validation")
}

case object Deployment extends WorkflowNode {
val id = WorkflowNodeId("Pending deployment")
val id: WorkflowNodeId = WorkflowNodeId("Pending deployment")
}

case object Deployed extends WorkflowNode {
val id = WorkflowNodeId("Deployed")
val id: WorkflowNodeId = WorkflowNodeId("Deployed")
}

case object Cancelled extends WorkflowNode {
val id = WorkflowNodeId("Cancelled")
val id: WorkflowNodeId = WorkflowNodeId("Cancelled")
}

val steps: List[WorkflowNode] = List(Validation, Deployment, Deployed, Cancelled)

/*
* Validation rule logic:
* - check for self validation (only needed if current user is the CR author)
* - check for current user rights.
* WARNING: WE ARE SIDE STEPPING authz check until https://issues.rudder.io/issues/22595 is solved
*/
private def canValidate(isCreator: Boolean, selfValidation: () => Box[Boolean]): Boolean = {
val correctActor = selfValidation().getOrElse(false) || !isCreator
correctActor && CurrentUser.checkRights(AuthorizationType.Validator.Edit)
}

private def canDeploy(isCreator: Boolean, selfDeployment: () => Box[Boolean]): Boolean = {
val correctActor = selfDeployment().getOrElse(false) || isCreator
correctActor && CurrentUser.checkRights(AuthorizationType.Deployer.Edit)
}

}

class TwoValidationStepsWorkflowServiceImpl(
Expand All @@ -140,27 +160,27 @@ class TwoValidationStepsWorkflowServiceImpl(

val closedSteps: List[WorkflowNodeId] = List(Cancelled.id, Deployed.id)
val openSteps: List[WorkflowNodeId] = List(Validation.id, Deployment.id)
val stepsValue = steps.map(_.id)
val stepsValue: List[WorkflowNodeId] = steps.map(_.id)

private[this] def saveAndLogChangeRequest(diff: ChangeRequestDiff, actor: EventActor, reason: Option[String]) = {
val changeRequest = diff.changeRequest
// We need to remap back to the original type to fetch the id of the CR created
val save = diff match {
case add: AddChangeRequestDiff =>
woChangeRequestRepository.createChangeRequest(diff.changeRequest, actor, reason).map(AddChangeRequestDiff(_))
case _: AddChangeRequestDiff =>
woChangeRequestRepository.createChangeRequest(diff.changeRequest, actor, reason).map(AddChangeRequestDiff)
case modify: ModifyToChangeRequestDiff =>
woChangeRequestRepository
.updateChangeRequest(changeRequest, actor, reason)
.map(x => modify) // For modification the id is already correct
case delete: DeleteChangeRequestDiff =>
woChangeRequestRepository.deleteChangeRequest(changeRequest.id, actor, reason).map(DeleteChangeRequestDiff(_))
.map(_ => modify) // For modification the id is already correct
case _: DeleteChangeRequestDiff =>
woChangeRequestRepository.deleteChangeRequest(changeRequest.id, actor, reason).map(DeleteChangeRequestDiff)
}

for {
saved <- save ?~! s"could not save change request ${changeRequest.info.name}"
modId = ModificationId(uuidGen.newUuid)
workflowEnable <- workflowEnable()
logged <- if (workflowEnable) {
_ <- if (workflowEnable) {
changeRequestEventLogService.saveChangeRequestLog(modId, actor, saved, reason) ?~!
s"could not save event log for change request ${saved.changeRequest.id} creation"
} else {
Expand All @@ -179,40 +199,37 @@ class TwoValidationStepsWorkflowServiceImpl(
saveAndLogChangeRequest(ModifyToChangeRequestDiff(newCr, oldChangeRequest), actor, reason)
}

/*
* Find available next steps for the current user.
* The given rights are expected to be the string representation of atomic permissions.
*/
def findNextSteps(
currentUserRights: Seq[String],
currentStep: WorkflowNodeId,
isCreator: Boolean
): WorkflowAction = {
val authorizedRoles = currentUserRights.filter(role => (role == "validator" || role == "deployer"))
// TODO: manage error for config !
val canValid = selfValidation().getOrElse(false) || !isCreator
val canDeploy = selfDeployment().getOrElse(false) || !isCreator
val deployAction = {
if (canDeploy(isCreator, selfDeployment))
Seq((Deployed.id, stepValidationToDeployed _))
else Seq()
}

currentStep match {
case Validation.id =>
val validatorActions = {
if (authorizedRoles.contains("validator") && canValid) {
Seq((Deployment.id, stepValidationToDeployment _)) ++ {
if (authorizedRoles.contains("deployer") && canDeploy)
Seq((Deployed.id, stepValidationToDeployed _))
else Seq()
}
} else Seq()
(if (canValidate(isCreator, selfValidation)) {
Seq((Deployment.id, stepValidationToDeployment _))
} else Seq()) ++ deployAction
}
WorkflowAction("Validate", validatorActions)

case Deployment.id =>
val actions = {
if (authorizedRoles.contains("deployer") && canDeploy)
Seq((Deployed.id, stepDeploymentToDeployed _))
else Seq()
}
WorkflowAction("Deploy", actions)
WorkflowAction("Deploy", deployAction)
case Deployed.id => NoWorkflowAction
case Cancelled.id => NoWorkflowAction
case WorkflowNodeId(x) =>
ChangeValidationLogger.warn(
s"An unknow workflow state was reached with ID: '${x}'. It is likely to be a bug, please report it"
s"An unknown workflow state was reached with ID: '${x}'. It is likely to be a bug, please report it"
)
NoWorkflowAction
}
Expand All @@ -223,30 +240,30 @@ class TwoValidationStepsWorkflowServiceImpl(
currentStep: WorkflowNodeId,
isCreator: Boolean
): Seq[(WorkflowNodeId, (ChangeRequestId, EventActor, Option[String]) => Box[WorkflowNodeId])] = {
val authorizedRoles = currentUserRights.filter(role => (role == "validator" || role == "deployer"))
// TODO: manage error for config !
val canValid = selfValidation().getOrElse(false) || !isCreator
val canDeploy = selfDeployment().getOrElse(false) || !isCreator
currentStep match {
case Validation.id =>
if (authorizedRoles.contains("validator") && canValid) Seq((Cancelled.id, stepValidationToCancelled _)) else Seq()
if (canValidate(isCreator, selfValidation))
Seq((Cancelled.id, stepValidationToCancelled))
else Seq()
case Deployment.id =>
if (authorizedRoles.contains("deployer") && canDeploy) Seq((Cancelled.id, stepDeploymentToCancelled _)) else Seq()
if (canDeploy(isCreator, selfDeployment))
Seq((Cancelled.id, stepDeploymentToCancelled))
else Seq()
case Deployed.id => Seq()
case Cancelled.id => Seq()
case WorkflowNodeId(x) =>
ChangeValidationLogger.warn(
s"An unknow workflow state was reached with ID: '${x}'. It is likely to be a bug, please report it"
s"An unknown workflow state was reached with ID: '${x}'. It is likely to be a bug, please report it"
)
Seq()
}
}

def isEditable(currentUserRights: Seq[String], currentStep: WorkflowNodeId, isCreator: Boolean): Boolean = {
val authorizedRoles = currentUserRights.filter(role => (role == "validator" || role == "deployer"))
val authorizedRoles = currentUserRights.filter(role => role == Role.Validator.name || role == Role.Deployer.name)
currentStep match {
case Validation.id => authorizedRoles.contains("validator") || isCreator
case Deployment.id => authorizedRoles.contains("deployer")
case Validation.id => authorizedRoles.contains(Role.Validator.name) || isCreator
case Deployment.id => authorizedRoles.contains(Role.Deployer.name)
case Deployed.id => false
case Cancelled.id => false
case WorkflowNodeId(x) =>
Expand Down Expand Up @@ -281,9 +298,9 @@ class TwoValidationStepsWorkflowServiceImpl(
def startWorkflow(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): Box[ChangeRequestId] = {
ChangeValidationLogger.debug(s"${name}: start workflow for change request '${changeRequest.id.value}'")
for {
saved <- saveAndLogChangeRequest(AddChangeRequestDiff(changeRequest), actor, reason)
workflow <- woWorkflowRepo.createWorkflow(saved.id, Validation.id)
_ = notificationService.sendNotification(Validation, saved).catchEmailError("changeRequestCreated", Validation.id.value)
saved <- saveAndLogChangeRequest(AddChangeRequestDiff(changeRequest), actor, reason)
_ <- woWorkflowRepo.createWorkflow(saved.id, Validation.id)
_ = notificationService.sendNotification(Validation, saved).catchEmailError("changeRequestCreated", Validation.id.value)
} yield {
workflowComet ! WorkflowUpdate
saved.id
Expand All @@ -300,7 +317,7 @@ class TwoValidationStepsWorkflowServiceImpl(
(for {
state <- woWorkflowRepo.updateState(changeRequestId, from.id, to.id)
workflowStep = WorkflowStepChange(changeRequestId, from.id, to.id)
log <- workflowLogger.saveEventLog(workflowStep, actor, reason)
_ <- workflowLogger.saveEventLog(workflowStep, actor, reason)
_ = sendEmail(from, to, changeRequestId).catchEmailError(from.id.value, to.id.value)
} yield {
workflowComet ! WorkflowUpdate
Expand Down Expand Up @@ -365,11 +382,11 @@ class TwoValidationStepsWorkflowServiceImpl(
): Box[WorkflowNodeId] = {
ChangeValidationLogger.debug(s"${name}: deploy change for change request '${changeRequestId.value}'")
for {
optcr <- roChangeRequestRepository.get(changeRequestId)
cr <- Box(optcr) ?~! s"Change request with ID '${changeRequestId.value}' was not found in database"
saved <- commit.save(cr, actor, reason)
repoOk <- woChangeRequestRepository.updateChangeRequest(saved, actor, reason)
state <- changeStep(from, Deployed, changeRequestId, actor, reason)
optcr <- roChangeRequestRepository.get(changeRequestId)
cr <- Box(optcr) ?~! s"Change request with ID '${changeRequestId.value}' was not found in database"
saved <- commit.save(cr, actor, reason)
_ <- woChangeRequestRepository.updateChangeRequest(saved, actor, reason)
state <- changeStep(from, Deployed, changeRequestId, actor, reason)
} yield {
state
}
Expand Down Expand Up @@ -410,14 +427,6 @@ class TwoValidationStepsWorkflowServiceImpl(
toFailure(Validation, changeRequestId, actor, reason)
}

private[this] def stepDeploymentToDeployed(
changeRequestId: ChangeRequestId,
actor: EventActor,
reason: Option[String]
): Box[WorkflowNodeId] = {
onSuccessWorkflow(Deployment, changeRequestId, actor, reason)
}

private[this] def stepDeploymentToCancelled(
changeRequestId: ChangeRequestId,
actor: EventActor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ import bootstrap.liftweb.RudderConfig
import bootstrap.rudder.plugin.ChangeValidationConf
import com.normation.eventlog.EventActor
import com.normation.eventlog.EventLog
import com.normation.plugins.changevalidation.ChangeValidationLogger
import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl
import com.normation.rudder.ActionType
import com.normation.rudder.AuthorizationType
import com.normation.rudder.domain.eventlog.AddChangeRequest
import com.normation.rudder.domain.eventlog.DeleteChangeRequest
Expand Down Expand Up @@ -86,11 +86,13 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable {
private[this] val commitAndDeployChangeRequest = RudderConfig.commitAndDeployChangeRequest

private[this] def checkAccess(cr: ChangeRequest) = {
CurrentUser.checkRights(AuthorizationType.Validator.Read) || CurrentUser.checkRights(
val check = CurrentUser.checkRights(AuthorizationType.Validator.Read) || CurrentUser.checkRights(
AuthorizationType.Deployer.Read
) || cr.owner == CurrentUser.actor.name
ChangeValidationLogger.trace(s"check user '${CurrentUser.actor.name}' access to change request '${cr.id}': ${check}")
check
}
private[this] val CrId: Box[Int] = { S.param("crId").map(x => x.toInt) }
private[this] val CrId: Box[Int] = { S.param("crId").map(x => x.toInt) }
private[this] var changeRequest: Box[ChangeRequest] = {
CrId match {
case Full(id) =>
Expand Down Expand Up @@ -167,12 +169,20 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable {
}

def displayActionButton(cr: ChangeRequest, step: WorkflowNodeId): NodeSeq = {
val authz = CurrentUser.getRights.authorizationTypes.toSeq.collect { case right: ActionType.Edit => right.authzKind }
val authz = Nil // we are sideStepping it, see: https://issues.rudder.io/issues/22595
val isOwner = cr.owner == CurrentUser.actor.name

("#backStep" #> {
workflowService.findBackSteps(authz, step, isOwner) match {
case Nil => NodeSeq.Empty
case Nil =>
ChangeValidationLogger.trace(
s"- no back step found for user '${CurrentUser.actor.name}' for CR #${cr.id.value} for step '${step}' (user is owner: ${isOwner})"
)
NodeSeq.Empty
case steps =>
ChangeValidationLogger.trace(
s"- back steps '${steps.map(_._1.value).mkString(", ")}' found for user '${CurrentUser.actor.name}' for CR #${cr.id.value} for step '${step}' (user is owner: ${isOwner})"
)
SHtml.ajaxButton(
"Decline",
() => ChangeStepPopup("Decline", steps, cr),
Expand All @@ -182,9 +192,20 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable {
} &
"#nextStep" #> {
workflowService.findNextSteps(authz, step, isOwner) match {
case NoWorkflowAction => NodeSeq.Empty
case WorkflowAction(actionName, emptyList) if emptyList.size == 0 => NodeSeq.Empty
case NoWorkflowAction =>
ChangeValidationLogger.trace(
s"- no next step found for user '${CurrentUser.actor.name}' for CR #${cr.id.value} for step '${step}' (user is owner: ${isOwner})"
)
NodeSeq.Empty
case WorkflowAction(actionName, emptyList) if emptyList.size == 0 =>
ChangeValidationLogger.trace(
s"- no next step found for user '${CurrentUser.actor.name}' for CR #${cr.id.value} for step '${step}' (user is owner: ${isOwner})"
)
NodeSeq.Empty
case WorkflowAction(actionName, steps) =>
ChangeValidationLogger.trace(
s"- next steps '${steps.map(_._1.value).mkString(", ")}' found for user '${CurrentUser.actor.name}' for CR #${cr.id.value} for step '${step}' (user is owner: ${isOwner})"
)
SHtml.ajaxButton(
actionName,
() => ChangeStepPopup(actionName, steps, cr),
Expand Down

0 comments on commit 3103cf4

Please sign in to comment.