forked from spinnaker/orca
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pipeline executions/orca) : Added ability to add roles to manual…
… judgment stage. (spinnaker#3988) * feat(pipeline executions/orca) : Added ability to add roles to manual judgment stage. This is part of: spinnaker/spinnaker#4792. Enhanced OperationsController.groovy to Get the application roles, stage roles and the user roles. Check each of the user roles whether they are contained in the stage and application roles. Once the user role is contained in the manual judgment stage. We check for whether the stage role has 'READ', 'WRITE', 'EXECUTE', 'CREATE' role in the application permissions, if the role has application permission as 'READ', then the user will not be allowed to proceed further to subsequent(downstream) stages. if the role has application permission as 'WRITE, EXECUTE,CREATE', then the user will be allowed to proceed further to subsequent stages. If yes/no, set a isAuthorized flag to true/false in each of the stage. By default, all the stages except Manual Judgment are true. Enhanced ManualJudgmentStage.groovy to Check for the flag in ManualJudgmentStage.groovy whether to execute to the next stage or not. If yes/no, continue with the next stages/continues running the same stage. Enhanced ManualJudgmentStageSpec.groovy to Modified the testcases as per the requirement. Added ManualJudgmentAuthzGroupsUtil.groovy Added a utlity method to check authorized groups of the manual judgment stage. Added ManualJudgmentAuthzGroupsUtilSpec.groovy Added test cases check authorized groups method of ManualJudgmentAuthzGroupsUtil.groovy. * feat(pipeline executions/orca) : Added ability to add roles to manual judgment stage. This is part of: spinnaker/spinnaker#4792. Enhanced OperationsController.groovy to Get the application roles, stage roles and the user roles. Check each of the user roles whether they are contained in the stage and application roles. Once the user role is contained in the manual judgment stage. We check for whether the stage role has 'READ', 'WRITE', 'EXECUTE', 'CREATE' role in the application permissions, if the role has application permission as 'READ', then the user will not be allowed to proceed further to subsequent(downstream) stages. if the role has application permission as 'WRITE, EXECUTE,CREATE', then the user will be allowed to proceed further to subsequent stages. If yes/no, set a isAuthorized flag to true/false in each of the stage. By default, all the stages except Manual Judgment are true. Enhanced ManualJudgmentStage.groovy to Check for the flag in ManualJudgmentStage.groovy whether to execute toThis is part of: spinnaker/spinnaker#4792. Enhanced OperationsController.groovy to tEnhanced OperationsController.groovy to GeeSGet the application roles, stage roless Enhanced ManualJudgmentStage.groovy to Check for the flag in ManualJudgmentStage.groovy whether to execute toThis is part of: spinnaker/spinnaker#4792. Enhanced OperationsController.groovy to tEnhanced OperationsController.groovy to GeeSGet the application roles, stage roless Co-authored-by: Adam Jordens <[email protected]>
- Loading branch information
Showing
7 changed files
with
306 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
...cho/src/main/java/com/netflix/spinnaker/orca/echo/util/ManualJudgmentAuthzGroupsUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* Copyright 2020 OpsMx, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.netflix.spinnaker.orca.echo.util; | ||
|
||
import static java.lang.String.format; | ||
|
||
import com.netflix.spinnaker.fiat.model.Authorization; | ||
import com.netflix.spinnaker.kork.exceptions.SpinnakerException; | ||
import com.netflix.spinnaker.orca.front50.Front50Service; | ||
import com.netflix.spinnaker.orca.front50.model.Application; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.http.HttpStatus; | ||
import retrofit.RetrofitError; | ||
|
||
public class ManualJudgmentAuthzGroupsUtil { | ||
|
||
Front50Service front50Service; | ||
|
||
@Autowired | ||
public ManualJudgmentAuthzGroupsUtil(Optional<Front50Service> front50Service) { | ||
this.front50Service = front50Service.orElse(null); | ||
} | ||
|
||
/** | ||
* This method checks if the logged in user has role in the manual judgment stage authorized | ||
* groups. We fetch the user roles and check if that role is authorized in the manual judgment | ||
* stage role. if the user role exists , then we check with the application permission roles. If | ||
* the application permission role has 'READ' then we return false(not authorized) If the | ||
* application permission role has 'CREATE, EXECUTE, WRITE' then we return true(authorized) | ||
* | ||
* @param userRoles | ||
* @param stageRoles | ||
* @param permissions | ||
* @return | ||
*/ | ||
public static boolean checkAuthorizedGroups( | ||
List<String> userRoles, List<String> stageRoles, Map<String, Object> permissions) { | ||
|
||
boolean isAuthorizedGroup = false; | ||
if (stageRoles == null || stageRoles.isEmpty()) { | ||
return true; | ||
} | ||
for (String role : userRoles) { // Fetches the userRoles of the logged in user | ||
if (stageRoles.contains( | ||
role)) { // Checks if the user role is authorized in the manual judgment stage. | ||
for (Map.Entry<String, Object> entry : | ||
permissions.entrySet()) { // get the application permission roles. | ||
if (Authorization.CREATE.name().equals(entry.getKey()) | ||
|| Authorization.EXECUTE.name().equals(entry.getKey()) | ||
|| Authorization.WRITE.name().equals(entry.getKey())) { | ||
// If the application permission roles has 'CREATE, EXECUTE, WRITE', then user is | ||
// authorized. | ||
if (entry.getValue() != null && ((List<String>) entry.getValue()).contains(role)) { | ||
return true; | ||
} | ||
} else if (Authorization.READ.name().equals(entry.getKey())) { | ||
// If the application permission roles has 'READ', then user is not authorized. | ||
if (entry.getValue() != null && ((List<String>) entry.getValue()).contains(role)) { | ||
isAuthorizedGroup = false; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return isAuthorizedGroup; | ||
} | ||
|
||
public Optional<Application> getApplication(String applicationName) { | ||
try { | ||
return Optional.of(front50Service.get(applicationName)); | ||
} catch (RetrofitError e) { | ||
if (e.getResponse().getStatus() == HttpStatus.NOT_FOUND.value()) { | ||
return Optional.empty(); | ||
} | ||
throw new SpinnakerException( | ||
format("Failed to retrieve application '%s'", applicationName), e); | ||
} catch (RuntimeException re) { | ||
return Optional.empty(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,9 +16,17 @@ | |
|
||
package com.netflix.spinnaker.orca.echo.pipeline | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import com.netflix.spinnaker.fiat.model.UserPermission | ||
import com.netflix.spinnaker.fiat.model.resources.Role | ||
import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator | ||
import com.netflix.spinnaker.fiat.shared.FiatStatus | ||
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus | ||
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution | ||
import com.netflix.spinnaker.orca.echo.EchoService | ||
import com.netflix.spinnaker.orca.echo.util.ManualJudgmentAuthzGroupsUtil | ||
import com.netflix.spinnaker.orca.front50.Front50Service | ||
import com.netflix.spinnaker.orca.front50.model.Application | ||
import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl | ||
import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl | ||
import spock.lang.Specification | ||
|
@@ -27,17 +35,41 @@ import static com.netflix.spinnaker.orca.echo.pipeline.ManualJudgmentStage.Notif | |
import static com.netflix.spinnaker.orca.echo.pipeline.ManualJudgmentStage.WaitForManualJudgmentTask | ||
|
||
class ManualJudgmentStageSpec extends Specification { | ||
|
||
EchoService echoService = Mock(EchoService) | ||
|
||
Front50Service front50Service = Mock(Front50Service) | ||
|
||
FiatPermissionEvaluator fpe = Mock(FiatPermissionEvaluator) | ||
|
||
FiatStatus fiatStatus = Mock() { | ||
_ * isEnabled() >> true | ||
} | ||
|
||
ManualJudgmentAuthzGroupsUtil manualJudgmentAuthzGroupsUtil = new ManualJudgmentAuthzGroupsUtil(Optional.of(front50Service)) | ||
|
||
ObjectMapper objectMapper = new ObjectMapper() | ||
|
||
def config = [ | ||
application: [ | ||
"name" : "orca", | ||
"owner" : "owner", | ||
"permissions" : [WRITE: ["foo"], READ: ["foo","baz"], EXECUTE: ["foo"]] | ||
], | ||
user : "testUser" | ||
] | ||
|
||
@Unroll | ||
void "should return execution status based on judgmentStatus"() { | ||
given: | ||
def task = new WaitForManualJudgmentTask() | ||
|
||
def task = new WaitForManualJudgmentTask(Optional.of(echoService), Optional.of(fpe), Optional.of(fiatStatus), | ||
Optional.of(objectMapper), Optional.of(manualJudgmentAuthzGroupsUtil)) | ||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", context)) | ||
|
||
then: | ||
1 * fiatStatus.isEnabled() >> { return false } | ||
result.status == expectedStatus | ||
result.context.isEmpty() | ||
|
||
where: | ||
context || expectedStatus | ||
|
@@ -49,9 +81,39 @@ class ManualJudgmentStageSpec extends Specification { | |
[judgmentStatus: "unknown"] || ExecutionStatus.RUNNING | ||
} | ||
|
||
@Unroll | ||
void "should return execution status based on authorizedGroups"() { | ||
given: | ||
1 * fpe.getPermission('[email protected]') >> { | ||
new UserPermission().addResources([new Role('foo'), new Role('baz')]).view | ||
} | ||
1 * front50Service.get("orca") >> new Application(config.application) | ||
|
||
def task = new WaitForManualJudgmentTask(Optional.of(echoService), Optional.of(fpe), Optional.of(fiatStatus), | ||
Optional.of(objectMapper), Optional.of(manualJudgmentAuthzGroupsUtil)) | ||
|
||
when: | ||
def stage = new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", context) | ||
stage.lastModified = new StageExecution.LastModifiedDetails(user: "[email protected]", allowedAccounts: ["group1"]) | ||
def result = task.execute(stage) | ||
|
||
then: | ||
result.status == expectedStatus | ||
|
||
where: | ||
context || expectedStatus | ||
[judgmentStatus: "continue", selectedStageRoles: ['foo']] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "Continue", selectedStageRoles: ['foo']] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "stop", selectedStageRoles: ['foo']] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "STOP", selectedStageRoles: ['foo']] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "Continue", selectedStageRoles: ['baz']] || ExecutionStatus.RUNNING | ||
[judgmentStatus: "Stop", selectedStageRoles: ['baz']] || ExecutionStatus.RUNNING | ||
} | ||
|
||
void "should only send notifications for supported types"() { | ||
given: | ||
def task = new WaitForManualJudgmentTask(echoService: Mock(EchoService)) | ||
def task = new WaitForManualJudgmentTask(Optional.of(echoService), Optional.of(fpe), Optional.of(fiatStatus), | ||
Optional.of(objectMapper), Optional.of(manualJudgmentAuthzGroupsUtil)) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", [notifications: [ | ||
|
@@ -72,7 +134,8 @@ class ManualJudgmentStageSpec extends Specification { | |
@Unroll | ||
void "if deprecated notification configuration is in use, only send notifications for awaiting judgment state"() { | ||
given: | ||
def task = new WaitForManualJudgmentTask(echoService: Mock(EchoService)) | ||
def task = new WaitForManualJudgmentTask(Optional.of(echoService), Optional.of(fpe), Optional.of(fiatStatus), | ||
Optional.of(objectMapper), Optional.of(manualJudgmentAuthzGroupsUtil)) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", [ | ||
|
@@ -84,6 +147,7 @@ class ManualJudgmentStageSpec extends Specification { | |
])) | ||
|
||
then: | ||
1 * fiatStatus.isEnabled() >> { return false } | ||
result.status == executionStatus | ||
if (sent) result.context.notifications?.getAt(0)?.lastNotifiedByNotificationState?.containsKey(notificationState) | ||
|
||
|
@@ -153,7 +217,8 @@ class ManualJudgmentStageSpec extends Specification { | |
@Unroll | ||
void "should retain unknown fields in the notification context"() { | ||
given: | ||
def task = new WaitForManualJudgmentTask(echoService: Mock(EchoService)) | ||
def task = new WaitForManualJudgmentTask(Optional.of(echoService), Optional.of(fpe), Optional.of(fiatStatus), | ||
Optional.of(objectMapper), Optional.of(manualJudgmentAuthzGroupsUtil)) | ||
|
||
def slackNotification = new Notification(type: "slack") | ||
slackNotification.setOther("customMessage", "hello slack") | ||
|
Oops, something went wrong.