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.
This PR verifies the current user roles against context.selectedStage…
…Roles. The operation is only allowed if there is at least one overlapping role. This is a rewrite of what originally went in as spinnaker#3988.
- Loading branch information
Showing
6 changed files
with
248 additions
and
30 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
95 changes: 95 additions & 0 deletions
95
...-echo/src/main/java/com/netflix/spinnaker/orca/echo/util/ManualJudgmentAuthorization.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,95 @@ | ||
/* | ||
* 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 com.google.common.base.Strings; | ||
import com.google.common.collect.Sets; | ||
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 java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.HashSet; | ||
import java.util.Optional; | ||
import java.util.stream.Collectors; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class ManualJudgmentAuthorization { | ||
private final Logger log = LoggerFactory.getLogger(getClass()); | ||
|
||
private final FiatPermissionEvaluator fiatPermissionEvaluator; | ||
|
||
private final FiatStatus fiatStatus; | ||
|
||
@Autowired | ||
public ManualJudgmentAuthorization( | ||
Optional<FiatPermissionEvaluator> fiatPermissionEvaluator, FiatStatus fiatStatus) { | ||
this.fiatPermissionEvaluator = fiatPermissionEvaluator.orElse(null); | ||
|
||
this.fiatStatus = fiatStatus; | ||
} | ||
|
||
/** | ||
* A manual judgment will be considered "authorized" if the current user has at least one of the | ||
* required judgment roles. | ||
* | ||
* @param application Application being judged | ||
* @param requiredJudgmentRoles Required judgment roles | ||
* @param currentUser User that has attempted this judgment | ||
* @return whether or not {@param currentUser} has authorization to judge | ||
*/ | ||
public boolean isAuthorized( | ||
String application, Collection<String> requiredJudgmentRoles, String currentUser) { | ||
if (!fiatStatus.isEnabled() || requiredJudgmentRoles.isEmpty()) { | ||
return true; | ||
} | ||
|
||
if (Strings.isNullOrEmpty(currentUser)) { | ||
return false; | ||
} | ||
|
||
UserPermission.View permission = fiatPermissionEvaluator.getPermission(currentUser); | ||
if (permission == null) { // Should never happen? | ||
log.warn("Attempted to get user permission for '{}' but none were found.", currentUser); | ||
return false; | ||
} | ||
|
||
return isAuthorized( | ||
requiredJudgmentRoles, | ||
permission.getRoles().stream().map(Role.View::getName).collect(Collectors.toList())); | ||
} | ||
|
||
private boolean isAuthorized( | ||
Collection<String> requiredJudgmentRoles, Collection<String> currentUserRoles) { | ||
if (requiredJudgmentRoles == null || requiredJudgmentRoles.isEmpty()) { | ||
return true; | ||
} | ||
|
||
if (currentUserRoles == null) { | ||
currentUserRoles = new ArrayList<>(); | ||
} | ||
|
||
return Sets.intersection(new HashSet<>(requiredJudgmentRoles), new HashSet<>(currentUserRoles)) | ||
.size() | ||
> 0; | ||
} | ||
} |
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,14 @@ | |
|
||
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.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.ManualJudgmentAuthorization | ||
import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl | ||
import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl | ||
import spock.lang.Specification | ||
|
@@ -27,17 +32,29 @@ 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 fiatPermissionEvaluator = Mock(FiatPermissionEvaluator) | ||
|
||
FiatStatus fiatStatus = Mock() { | ||
_ * isEnabled() >> true | ||
} | ||
|
||
ManualJudgmentAuthorization manualJudgmentAuthorization = new ManualJudgmentAuthorization( | ||
Optional.of(fiatPermissionEvaluator), | ||
fiatStatus | ||
) | ||
|
||
@Unroll | ||
void "should return execution status based on judgmentStatus"() { | ||
given: | ||
def task = new WaitForManualJudgmentTask() | ||
def task = new WaitForManualJudgmentTask(Optional.of(echoService), manualJudgmentAuthorization) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", context)) | ||
|
||
then: | ||
result.status == expectedStatus | ||
result.context.isEmpty() | ||
|
||
where: | ||
context || expectedStatus | ||
|
@@ -49,9 +66,36 @@ class ManualJudgmentStageSpec extends Specification { | |
[judgmentStatus: "unknown"] || ExecutionStatus.RUNNING | ||
} | ||
|
||
@Unroll | ||
void "should return execution status based on authorizedGroups"() { | ||
given: | ||
1 * fiatPermissionEvaluator.getPermission('[email protected]') >> { | ||
new UserPermission().addResources([new Role('foo')]).view | ||
} | ||
|
||
def task = new WaitForManualJudgmentTask(Optional.of(echoService), manualJudgmentAuthorization) | ||
|
||
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), manualJudgmentAuthorization) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", [notifications: [ | ||
|
@@ -72,7 +116,7 @@ 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), manualJudgmentAuthorization) | ||
|
||
when: | ||
def result = task.execute(new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "", [ | ||
|
@@ -153,7 +197,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), manualJudgmentAuthorization) | ||
|
||
def slackNotification = new Notification(type: "slack") | ||
slackNotification.setOther("customMessage", "hello slack") | ||
|
53 changes: 53 additions & 0 deletions
53
...c/test/groovy/com/netflix/spinnaker/orca/echo/util/ManualJudgmentAuthorizationSpec.groovy
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,53 @@ | ||
/* | ||
* 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 com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator | ||
import com.netflix.spinnaker.fiat.shared.FiatStatus | ||
import spock.lang.Specification | ||
import spock.lang.Subject | ||
import spock.lang.Unroll | ||
|
||
class ManualJudgmentAuthorizationSpec extends Specification { | ||
def fiatPermissionEvaluator = Mock(FiatPermissionEvaluator) | ||
def fiatStatus = Mock(FiatStatus) | ||
|
||
@Subject | ||
def manualJudgmentAuthorization = new ManualJudgmentAuthorization( | ||
Optional.of(fiatPermissionEvaluator), | ||
fiatStatus | ||
) | ||
|
||
@Unroll | ||
void 'should determine authorization based on intersection of userRoles and stageRoles/permissions'() { | ||
when: | ||
def result = manualJudgmentAuthorization.isAuthorized(requiredJudgmentRoles, currentUserRoles) | ||
|
||
then: | ||
result == isAuthorized | ||
|
||
where: | ||
requiredJudgmentRoles | currentUserRoles || isAuthorized | ||
['foo', 'blaz'] | ['foo', 'baz'] || true | ||
[] | ['foo', 'baz'] || true | ||
[] | [] || true | ||
['foo'] | ['foo'] || true | ||
['foo'] | [] || false | ||
['foo'] | null || false | ||
null | null || true | ||
} | ||
} |
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