Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
public enum SingularityAuthorizationScope {
READ,
WRITE,
ADMIN,
DEPLOY
ADMIN
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class SingularityRequest {
private final Optional<String> requiredRole;
private final Optional<Set<String>> readWriteGroups;
private final Optional<Set<String>> readOnlyGroups;
private final Optional<Map<String, Set<SingularityUserFacingAction>>> actionPermissions;
private final Optional<Boolean> bounceAfterScale;
private final Optional<Map<SingularityEmailType, List<SingularityEmailDestination>>> emailConfigurationOverrides;
private final Optional<Boolean> hideEvenNumberAcrossRacksHint;
Expand Down Expand Up @@ -93,6 +94,9 @@ public SingularityRequest(
@JsonProperty("group") Optional<String> group,
@JsonProperty("readWriteGroups") Optional<Set<String>> readWriteGroups,
@JsonProperty("readOnlyGroups") Optional<Set<String>> readOnlyGroups,
@JsonProperty(
"actionPermissions"
) Optional<Map<String, Set<SingularityUserFacingAction>>> actionPermissions,
@JsonProperty("bounceAfterScale") Optional<Boolean> bounceAfterScale,
@JsonProperty("skipHealthchecks") Optional<Boolean> skipHealthchecks,
@JsonProperty(
Expand Down Expand Up @@ -155,6 +159,7 @@ public SingularityRequest(
this.requiredRole = requiredRole;
this.readWriteGroups = readWriteGroups;
this.readOnlyGroups = readOnlyGroups;
this.actionPermissions = actionPermissions;
this.bounceAfterScale = bounceAfterScale;
this.emailConfigurationOverrides = emailConfigurationOverrides;
this.skipHealthchecks = skipHealthchecks;
Expand Down Expand Up @@ -199,6 +204,7 @@ public SingularityRequestBuilder toBuilder() {
.setGroup(group)
.setReadWriteGroups(readWriteGroups)
.setReadOnlyGroups(readOnlyGroups)
.setActionPermissions(actionPermissions)
.setBounceAfterScale(bounceAfterScale)
.setEmailConfigurationOverrides(emailConfigurationOverrides)
.setSkipHealthchecks(skipHealthchecks)
Expand Down Expand Up @@ -502,6 +508,11 @@ public Optional<Set<String>> getReadOnlyGroups() {
return readOnlyGroups;
}

@Schema(nullable = true, description = "Permissions for specific groups")
public Optional<Map<String, Set<SingularityUserFacingAction>>> getActionPermissions() {
return actionPermissions;
}

@Schema(
nullable = true,
description = "Used for SingularityUI. If true, automatically trigger a bounce after changing the request's instance count"
Expand Down Expand Up @@ -605,6 +616,7 @@ public boolean equals(Object o) {
Objects.equals(requiredRole, that.requiredRole) &&
Objects.equals(readWriteGroups, that.readWriteGroups) &&
Objects.equals(readOnlyGroups, that.readOnlyGroups) &&
Objects.equals(actionPermissions, that.actionPermissions) &&
Objects.equals(bounceAfterScale, that.bounceAfterScale) &&
Objects.equals(emailConfigurationOverrides, that.emailConfigurationOverrides) &&
Objects.equals(hideEvenNumberAcrossRacksHint, that.hideEvenNumberAcrossRacksHint) &&
Expand Down Expand Up @@ -648,6 +660,7 @@ public int hashCode() {
requiredRole,
readWriteGroups,
readOnlyGroups,
actionPermissions,
bounceAfterScale,
emailConfigurationOverrides,
hideEvenNumberAcrossRacksHint,
Expand Down Expand Up @@ -715,6 +728,8 @@ public String toString() {
readWriteGroups +
", readOnlyGroups=" +
readOnlyGroups +
", actionPermissions=" +
actionPermissions +
", bounceAfterScale=" +
bounceAfterScale +
", emailConfigurationOverrides=" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class SingularityRequestBuilder {
private Optional<String> group;
private Optional<Set<String>> readWriteGroups;
private Optional<Set<String>> readOnlyGroups;
private Optional<Map<String, Set<SingularityUserFacingAction>>> actionPermissions;
private Optional<Boolean> bounceAfterScale;
private Optional<Map<SingularityEmailType, List<SingularityEmailDestination>>> emailConfigurationOverrides;
private Optional<Boolean> hideEvenNumberAcrossRacksHint;
Expand Down Expand Up @@ -78,6 +79,7 @@ public SingularityRequestBuilder(String id, RequestType requestType) {
this.group = Optional.empty();
this.readWriteGroups = Optional.empty();
this.readOnlyGroups = Optional.empty();
this.actionPermissions = Optional.empty();
this.bounceAfterScale = Optional.empty();
this.emailConfigurationOverrides = Optional.empty();
this.skipHealthchecks = Optional.empty();
Expand Down Expand Up @@ -116,6 +118,7 @@ public SingularityRequest build() {
group,
readWriteGroups,
readOnlyGroups,
actionPermissions,
bounceAfterScale,
skipHealthchecks,
emailConfigurationOverrides,
Expand Down Expand Up @@ -352,6 +355,17 @@ public SingularityRequestBuilder setReadWriteGroups(
return this;
}

public Optional<Map<String, Set<SingularityUserFacingAction>>> getActionPermissions() {
return actionPermissions;
}

public SingularityRequestBuilder setActionPermissions(
Optional<Map<String, Set<SingularityUserFacingAction>>> getActionPermissions
) {
this.actionPermissions = getActionPermissions;
return this;
}

public SingularityRequestBuilder setRequiredAgentAttributes(
Optional<Map<String, String>> requiredAgentAttributes
) {
Expand Down Expand Up @@ -551,6 +565,7 @@ public boolean equals(Object o) {
Objects.equals(group, that.group) &&
Objects.equals(readWriteGroups, that.readWriteGroups) &&
Objects.equals(readOnlyGroups, that.readOnlyGroups) &&
Objects.equals(actionPermissions, that.actionPermissions) &&
Objects.equals(bounceAfterScale, that.bounceAfterScale) &&
Objects.equals(emailConfigurationOverrides, that.emailConfigurationOverrides) &&
Objects.equals(hideEvenNumberAcrossRacksHint, that.hideEvenNumberAcrossRacksHint) &&
Expand Down Expand Up @@ -594,6 +609,7 @@ public int hashCode() {
group,
readWriteGroups,
readOnlyGroups,
actionPermissions,
bounceAfterScale,
emailConfigurationOverrides,
hideEvenNumberAcrossRacksHint,
Expand Down Expand Up @@ -661,6 +677,8 @@ public String toString() {
readWriteGroups +
", readOnlyGroups=" +
readOnlyGroups +
", actionPermissions=" +
actionPermissions +
", bounceAfterScale=" +
bounceAfterScale +
", emailConfigurationOverrides=" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.hubspot.singularity;

public enum SingularityUserFacingAction {
EXEC,
BOUNCE,
PAUSE,
SCALE,
KILL_TASK,
DELETE_SCHEDULED_TASK,
RUN_SHELL_COMMAND
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.hubspot.singularity.SingularityUserFacingAction;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand All @@ -13,20 +15,25 @@ public class SingularityUpdateGroupsRequest {
private final Optional<String> group;
private final Set<String> readWriteGroups;
private final Set<String> readOnlyGroups;
private final Optional<Map<String, Set<SingularityUserFacingAction>>> actionPermissions;
private final Optional<String> message;

@JsonCreator
public SingularityUpdateGroupsRequest(
@JsonProperty("group") Optional<String> group,
@JsonProperty("readWriteGroups") Set<String> readWriteGroups,
@JsonProperty("readOnlyGroups") Set<String> readOnlyGroups,
@JsonProperty(
"actionPermissions"
) Optional<Map<String, Set<SingularityUserFacingAction>>> actionPermissions,
@JsonProperty("message") Optional<String> message
) {
this.group = group;
this.readWriteGroups =
readWriteGroups != null ? readWriteGroups : Collections.emptySet();
this.readOnlyGroups =
readOnlyGroups != null ? readOnlyGroups : Collections.emptySet();
this.actionPermissions = actionPermissions;
this.message = message;
}

Expand All @@ -45,6 +52,11 @@ public Set<String> getReadOnlyGroups() {
return readOnlyGroups;
}

@Schema(description = "Overidden scopes for specific groups")
public Optional<Map<String, Set<SingularityUserFacingAction>>> getActionPermissions() {
return actionPermissions;
}

@Schema(
description = "An option message detailing the reason for the group updates",
nullable = true
Expand All @@ -64,6 +76,7 @@ public boolean equals(Object obj) {
Objects.equals(this.group, that.group) &&
Objects.equals(this.readWriteGroups, that.readWriteGroups) &&
Objects.equals(this.readOnlyGroups, that.readOnlyGroups) &&
Objects.equals(this.actionPermissions, that.actionPermissions) &&
Objects.equals(this.message, that.message)
);
}
Expand All @@ -72,7 +85,13 @@ public boolean equals(Object obj) {

@Override
public int hashCode() {
return Objects.hash(group, readWriteGroups, readOnlyGroups, message);
return Objects.hash(
group,
readWriteGroups,
readOnlyGroups,
actionPermissions,
message
);
}

@Override
Expand All @@ -85,6 +104,8 @@ public String toString() {
readWriteGroups +
", readOnlyGroups=" +
readOnlyGroups +
", groupScopeOverrides=" +
actionPermissions +
", message=" +
message +
'}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.hubspot.singularity.SingularityRequestWithState;
import com.hubspot.singularity.SingularityTaskId;
import com.hubspot.singularity.SingularityUser;
import com.hubspot.singularity.SingularityUserFacingAction;
import com.hubspot.singularity.data.RequestManager;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -42,16 +43,52 @@ static boolean groupsIntersect(Set<String> a, Set<String> b) {

public abstract void checkReadAuthorization(SingularityUser user);

public abstract void checkForAuthorization(
public void checkForAuthorization(
SingularityRequest request,
SingularityUser user,
SingularityAuthorizationScope scope
) {
checkForAuthorization(request, user, scope, Optional.empty());
}

public void checkForAuthorization(
SingularityRequest request,
SingularityUser user,
SingularityAuthorizationScope scope,
SingularityUserFacingAction action
) {
checkForAuthorization(request, user, scope, Optional.of(action));
}

protected abstract void checkForAuthorization(
SingularityRequest request,
SingularityUser user,
SingularityAuthorizationScope scope,
Optional<SingularityUserFacingAction> action
);

public abstract boolean isAuthorizedForRequest(
public boolean isAuthorizedForRequest(
SingularityRequest request,
SingularityUser user,
SingularityAuthorizationScope scope
) {
return isAuthorizedForRequest(request, user, scope, Optional.empty());
}

public boolean isAuthorizedForRequest(
SingularityRequest request,
SingularityUser user,
SingularityAuthorizationScope scope,
SingularityUserFacingAction action
) {
return isAuthorizedForRequest(request, user, scope, Optional.of(action));
}

protected abstract boolean isAuthorizedForRequest(
SingularityRequest request,
SingularityUser user,
SingularityAuthorizationScope scope,
Optional<SingularityUserFacingAction> action
);

public abstract void checkForAuthorizedChanges(
Expand All @@ -64,6 +101,24 @@ public void checkForAuthorizationByTaskId(
String taskId,
SingularityUser user,
SingularityAuthorizationScope scope
) {
checkForAuthorizationByTaskId(taskId, user, scope, Optional.empty());
}

public void checkForAuthorizationByTaskId(
String taskId,
SingularityUser user,
SingularityAuthorizationScope scope,
SingularityUserFacingAction action
) {
checkForAuthorizationByTaskId(taskId, user, scope, Optional.of(action));
}

private void checkForAuthorizationByTaskId(
String taskId,
SingularityUser user,
SingularityAuthorizationScope scope,
Optional<SingularityUserFacingAction> action
) {
if (authEnabled) {
checkForbidden(user.isAuthenticated(), "Not Authenticated!");
Expand All @@ -76,7 +131,12 @@ public void checkForAuthorizationByTaskId(

maybeRequest.ifPresent(
singularityRequestWithState ->
checkForAuthorization(singularityRequestWithState.getRequest(), user, scope)
checkForAuthorization(
singularityRequestWithState.getRequest(),
user,
scope,
action
)
);
} catch (InvalidSingularityTaskIdException e) {
badRequest(e.getMessage());
Expand All @@ -88,6 +148,24 @@ public void checkForAuthorizationByRequestId(
String requestId,
SingularityUser user,
SingularityAuthorizationScope scope
) {
checkForAuthorizationByRequestId(requestId, user, scope, Optional.empty());
}

public void checkForAuthorizationByRequestId(
String requestId,
SingularityUser user,
SingularityAuthorizationScope scope,
SingularityUserFacingAction action
) {
checkForAuthorizationByRequestId(requestId, user, scope, Optional.of(action));
}

public void checkForAuthorizationByRequestId(
String requestId,
SingularityUser user,
SingularityAuthorizationScope scope,
Optional<SingularityUserFacingAction> action
) {
if (authEnabled) {
final Optional<SingularityRequestWithState> maybeRequest = requestManager.getRequest(
Expand All @@ -96,7 +174,12 @@ public void checkForAuthorizationByRequestId(

maybeRequest.ifPresent(
singularityRequestWithState ->
checkForAuthorization(singularityRequestWithState.getRequest(), user, scope)
checkForAuthorization(
singularityRequestWithState.getRequest(),
user,
scope,
action
)
);
}
}
Expand Down
Loading