-
Notifications
You must be signed in to change notification settings - Fork 811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(pipeline executions/orca) : Added ability to add roles to manual… #3983
base: master
Are you sure you want to change the base?
Changes from 1 commit
7a15388
c13a948
51d70a3
9ea608b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,23 +19,26 @@ package com.netflix.spinnaker.orca.echo.pipeline | |
import com.fasterxml.jackson.annotation.JsonAnyGetter | ||
import com.fasterxml.jackson.annotation.JsonAnySetter | ||
import com.fasterxml.jackson.annotation.JsonIgnore | ||
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus | ||
import com.google.common.annotations.VisibleForTesting | ||
import com.google.common.base.Strings | ||
import com.netflix.spinnaker.fiat.model.UserPermission | ||
import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator | ||
import com.netflix.spinnaker.orca.AuthenticatedStage | ||
import com.netflix.spinnaker.orca.api.pipeline.OverridableTimeoutRetryableTask | ||
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution | ||
import com.netflix.spinnaker.orca.api.pipeline.TaskResult | ||
|
||
import javax.annotation.Nonnull | ||
import java.util.concurrent.TimeUnit | ||
import com.google.common.annotations.VisibleForTesting | ||
import com.netflix.spinnaker.orca.* | ||
import com.netflix.spinnaker.orca.echo.EchoService | ||
import com.netflix.spinnaker.orca.api.pipeline.graph.StageDefinitionBuilder | ||
import com.netflix.spinnaker.orca.api.pipeline.graph.TaskNode | ||
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.security.User | ||
import groovy.util.logging.Slf4j | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.stereotype.Component | ||
|
||
import javax.annotation.Nonnull | ||
import java.util.concurrent.TimeUnit | ||
|
||
@Component | ||
class ManualJudgmentStage implements StageDefinitionBuilder, AuthenticatedStage { | ||
|
||
|
@@ -72,21 +75,40 @@ class ManualJudgmentStage implements StageDefinitionBuilder, AuthenticatedStage | |
final long backoffPeriod = 15000 | ||
final long timeout = TimeUnit.DAYS.toMillis(3) | ||
|
||
@Autowired(required = false) | ||
EchoService echoService | ||
private final EchoService echoService | ||
|
||
private final FiatPermissionEvaluator fiatPermissionEvaluator | ||
|
||
WaitForManualJudgmentTask() { | ||
} | ||
|
||
WaitForManualJudgmentTask(Optional<FiatPermissionEvaluator> fpe) { | ||
this.fiatPermissionEvaluator = fpe.orElse(null) | ||
} | ||
|
||
WaitForManualJudgmentTask(Optional<EchoService> echoService, Optional<FiatPermissionEvaluator> fpe) { | ||
this.echoService = echoService.orElse(null) | ||
this.fiatPermissionEvaluator = fpe.orElse(null) | ||
} | ||
|
||
@Override | ||
TaskResult execute(StageExecution stage) { | ||
StageData stageData = stage.mapTo(StageData) | ||
def stageAuthorized = stage.context.get('isAuthorized') | ||
def stageRoles = stage.context.get('stageRoles') | ||
def permissions = stage.context.get('permissions') | ||
def username = stage.lastModified? stage.lastModified.user: stage.context.get('username') | ||
String notificationState | ||
ExecutionStatus executionStatus | ||
|
||
switch (stageData.state) { | ||
case StageData.State.CONTINUE: | ||
stageAuthorized = stageAuthorized || checkManualJudgmentAuthorizedGroups(stageRoles, permissions, username) | ||
notificationState = "manualJudgmentContinue" | ||
executionStatus = ExecutionStatus.SUCCEEDED | ||
break | ||
case StageData.State.STOP: | ||
stageAuthorized = stageAuthorized || checkManualJudgmentAuthorizedGroups(stageRoles, permissions, username) | ||
notificationState = "manualJudgmentStop" | ||
executionStatus = ExecutionStatus.TERMINAL | ||
break | ||
|
@@ -95,12 +117,31 @@ class ManualJudgmentStage implements StageDefinitionBuilder, AuthenticatedStage | |
executionStatus = ExecutionStatus.RUNNING | ||
break | ||
} | ||
|
||
if (!stageAuthorized) { | ||
notificationState = "manualJudgment" | ||
executionStatus = ExecutionStatus.RUNNING | ||
stage.context.put("judgmentStatus", "") | ||
} | ||
Map outputs = processNotifications(stage, stageData, notificationState) | ||
|
||
return TaskResult.builder(executionStatus).context(outputs).build() | ||
} | ||
|
||
boolean checkManualJudgmentAuthorizedGroups(def stageRoles, def permissions, def username) { | ||
|
||
if (!Strings.isNullOrEmpty(username)) { | ||
UserPermission.View permission = fiatPermissionEvaluator.getPermission(username); | ||
if (permission == null) { // Should never happen? | ||
return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this should never happen, I presume we want to know when it does happen? Perhaps would be useful to have a warn-level log about it?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
} | ||
// User has to have all the pipeline roles. | ||
def userRoles = permission.getRoles().collect { it.getName().trim() } | ||
return ManualJudgmentAuthzGroupsUtil.checkAuthorizedGroups(userRoles, stageRoles, permissions) | ||
} else { | ||
return false | ||
} | ||
} | ||
|
||
Map processNotifications(StageExecution stage, StageData stageData, String notificationState) { | ||
if (echoService) { | ||
// sendNotifications will be true if using the new scheme for configuration notifications. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convert to Java: We don't accept new files in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
* Copyright 2020 Netflix, Inc. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably update this new file's copyright such that it reflects OpsMx's legal name instead of Netflix. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
* | ||
* 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 com.netflix.spinnaker.fiat.model.Authorization; | ||
|
||
public class ManualJudgmentAuthzGroupsUtil { | ||
|
||
public static boolean checkAuthorizedGroups(def userRoles, def stageRoles, | ||
def permissions) { | ||
|
||
def isAuthorizedGroup = false | ||
if (!stageRoles) { | ||
return true | ||
} | ||
for (role in userRoles) { | ||
if (stageRoles.contains(role)) { | ||
for (perm in permissions) { | ||
def permKey = perm.getKey() | ||
if (Authorization.CREATE.name().equals(permKey) || | ||
Authorization.EXECUTE.name().equals(permKey) || | ||
Authorization.WRITE.name().equals(permKey)) { | ||
if (perm.getValue() && perm.getValue().contains(role)) { | ||
return true | ||
} | ||
} else if (Authorization.READ.name().equals(permKey)) { | ||
if (perm.getValue() && perm.getValue().contains(role)) { | ||
isAuthorizedGroup = false | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return isAuthorizedGroup | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,9 @@ | |
|
||
package com.netflix.spinnaker.orca.echo.pipeline | ||
|
||
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.orca.api.pipeline.models.ExecutionStatus | ||
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution | ||
import com.netflix.spinnaker.orca.echo.EchoService | ||
|
@@ -27,6 +30,11 @@ 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) | ||
|
||
FiatPermissionEvaluator fpe = Mock(FiatPermissionEvaluator) | ||
|
||
@Unroll | ||
void "should return execution status based on judgmentStatus"() { | ||
given: | ||
|
@@ -42,16 +50,46 @@ class ManualJudgmentStageSpec extends Specification { | |
where: | ||
context || expectedStatus | ||
[:] || ExecutionStatus.RUNNING | ||
[judgmentStatus: "continue"] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "Continue"] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "stop"] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "STOP"] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "unknown"] || ExecutionStatus.RUNNING | ||
[judgmentStatus: "continue", isAuthorized: true] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "Continue", isAuthorized: true] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "stop", isAuthorized: true] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "STOP", isAuthorized: true] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "unknown", isAuthorized: true] || ExecutionStatus.RUNNING | ||
} | ||
|
||
@Unroll | ||
void "should return execution status based on authorizedGroups"() { | ||
given: | ||
def task = new WaitForManualJudgmentTask(Optional.of(fpe)) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", context)) | ||
|
||
then: | ||
1 * fpe.getPermission('[email protected]') >> { | ||
new UserPermission().addResources([new Role('foo'), new Role('baz')]).view | ||
} | ||
result.status == expectedStatus | ||
|
||
where: | ||
context || expectedStatus | ||
[judgmentStatus: "continue", isAuthorized: false, username: "[email protected]", | ||
stageRoles: "foo", permissions: [WRITE: "foo",READ: "foo",READ: "baz"]] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "Continue", isAuthorized: false, username: "[email protected]", | ||
stageRoles: "foo", permissions: [WRITE: "foo",READ: "foo",READ: "baz"]] || ExecutionStatus.SUCCEEDED | ||
[judgmentStatus: "stop", isAuthorized: false, username: "[email protected]", | ||
stageRoles: "foo", permissions: [WRITE: "foo",READ: "foo",READ: "baz"]] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "STOP", isAuthorized: false, username: "[email protected]", | ||
stageRoles: "foo", permissions: [WRITE: "foo",READ: "foo",READ: "baz"]] || ExecutionStatus.TERMINAL | ||
[judgmentStatus: "Continue", isAuthorized: false, username: "[email protected]", | ||
stageRoles: "baz", permissions: [WRITE: "foo",READ: "foo",READ: "baz"]] || ExecutionStatus.RUNNING | ||
[judgmentStatus: "Stop", isAuthorized: false, username: "[email protected]", | ||
stageRoles: "baz", permissions: [WRITE: "foo",READ: "foo",READ: "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)) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", [notifications: [ | ||
|
@@ -72,14 +110,15 @@ 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)) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", [ | ||
sendNotifications: sendNotifications, | ||
notifications: [ | ||
new Notification(type: "email", address: "[email protected]", when: [ notificationState ]) | ||
], | ||
isAuthorized: true, | ||
judgmentStatus: judgmentStatus | ||
])) | ||
|
||
|
@@ -153,7 +192,7 @@ 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)) | ||
|
||
def slackNotification = new Notification(type: "slack") | ||
slackNotification.setOther("customMessage", "hello slack") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,24 +16,27 @@ | |
|
||
package com.netflix.spinnaker.orca.controllers | ||
|
||
import com.fasterxml.jackson.core.type.TypeReference | ||
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.FiatService | ||
import com.netflix.spinnaker.fiat.shared.FiatStatus | ||
import com.netflix.spinnaker.kork.exceptions.ConfigurationException | ||
import com.netflix.spinnaker.kork.exceptions.SpinnakerException | ||
import com.netflix.spinnaker.kork.exceptions.UserException | ||
import com.netflix.spinnaker.orca.api.pipeline.models.PipelineExecution | ||
import com.netflix.spinnaker.orca.api.pipeline.models.Trigger | ||
import com.netflix.spinnaker.orca.clouddriver.service.JobService | ||
import com.netflix.spinnaker.orca.echo.util.ManualJudgmentAuthzGroupsUtil | ||
import com.netflix.spinnaker.orca.exceptions.OperationFailedException | ||
import com.netflix.spinnaker.orca.exceptions.PipelineTemplateValidationException | ||
import com.netflix.spinnaker.orca.extensionpoint.pipeline.ExecutionPreprocessor | ||
import com.netflix.spinnaker.orca.front50.Front50Service | ||
import com.netflix.spinnaker.orca.front50.PipelineModelMutator | ||
import com.netflix.spinnaker.orca.front50.model.Application | ||
import com.netflix.spinnaker.orca.igor.BuildService | ||
import com.netflix.spinnaker.orca.pipeline.ExecutionLauncher | ||
import com.netflix.spinnaker.orca.api.pipeline.models.Trigger | ||
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionNotFoundException | ||
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository | ||
import com.netflix.spinnaker.orca.pipeline.util.ArtifactUtils | ||
|
@@ -44,19 +47,15 @@ import com.netflix.spinnaker.security.AuthenticatedRequest | |
import groovy.util.logging.Slf4j | ||
import javassist.NotFoundException | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.web.bind.annotation.PathVariable | ||
import org.springframework.web.bind.annotation.RequestBody | ||
import org.springframework.web.bind.annotation.RequestMapping | ||
import org.springframework.web.bind.annotation.RequestMethod | ||
import org.springframework.web.bind.annotation.RestController | ||
import org.springframework.web.bind.annotation.* | ||
import retrofit.RetrofitError | ||
import retrofit.http.Query | ||
|
||
import javax.servlet.http.HttpServletResponse | ||
|
||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND; | ||
import static com.netflix.spinnaker.orca.api.pipeline.models.ExecutionType.ORCHESTRATION | ||
import static com.netflix.spinnaker.orca.api.pipeline.models.ExecutionType.PIPELINE | ||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND | ||
import static net.logstash.logback.argument.StructuredArguments.value | ||
|
||
@RestController | ||
|
@@ -104,6 +103,9 @@ class OperationsController { | |
@Autowired(required = false) | ||
Front50Service front50Service | ||
|
||
@Autowired(required = false) | ||
private FiatPermissionEvaluator fiatPermissionEvaluator | ||
|
||
@RequestMapping(value = "/orchestrate", method = RequestMethod.POST) | ||
Map<String, Object> orchestrate(@RequestBody Map pipeline, HttpServletResponse response) { | ||
return planOrOrchestratePipeline(pipeline) | ||
|
@@ -173,7 +175,7 @@ class OperationsController { | |
private Map<String, Object> orchestratePipeline(Map pipeline) { | ||
long startTime = System.currentTimeMillis() | ||
def request = objectMapper.writeValueAsString(pipeline) | ||
|
||
addStageAuthorizedRoles(request,pipeline) | ||
Exception pipelineError = null | ||
try { | ||
pipeline = parseAndValidatePipeline(pipeline) | ||
|
@@ -204,6 +206,37 @@ class OperationsController { | |
} | ||
} | ||
|
||
private void addStageAuthorizedRoles(def request, Map pipeline) { | ||
|
||
def applicationName = pipeline.application | ||
if (applicationName) { | ||
Application application = front50Service.get(applicationName) | ||
if (application) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This usage is still incorrect - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
def username = AuthenticatedRequest.getSpinnakerUser().orElse("") | ||
if (application.getPermission().permissions && application.getPermission().permissions.permissions) { | ||
def permissions = objectMapper.convertValue(application.getPermission().permissions.permissions, | ||
new TypeReference<Map<String, Object>>() {}) | ||
UserPermission.View permission = fiatPermissionEvaluator.getPermission(username); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
if (permission == null) { // Should never happen? | ||
return; | ||
} | ||
// User has to have all the pipeline roles. | ||
Set<Role.View> roleView = permission.getRoles() | ||
def userRoles = [] | ||
roleView.each { it -> userRoles.add(it.getName().trim()) } | ||
def stageList = pipeline.stages | ||
def stageRoles = [] | ||
stageList.each { item -> | ||
stageRoles = item.selectedStageRoles | ||
item.isAuthorized = ManualJudgmentAuthzGroupsUtil.checkAuthorizedGroups(userRoles, stageRoles, permissions) | ||
item.stageRoles = stageRoles | ||
item.permissions = permissions | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private void recordPipelineFailure(Map pipeline, String errorMessage) { | ||
// While we are recording the failure for this execution, we still want to | ||
// parse/validate/realize the pipeline as best as we can. This way the UI | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove both. Neither of these constructors are necessary and would cause issues when doing dependency injection because none of them are annotated with
@Autowired
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done