package com.hubspot.singularity.auth; import static com.google.common.collect.ImmutableSet.copyOf; import static com.hubspot.singularity.WebExceptions.badRequest; import static com.hubspot.singularity.WebExceptions.checkForbidden; import static com.hubspot.singularity.WebExceptions.checkNotFound; import static com.hubspot.singularity.WebExceptions.checkUnauthorized; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Singleton; import com.hubspot.mesos.JavaUtils; import com.hubspot.singularity.InvalidSingularityTaskIdException; import com.hubspot.singularity.SingularityAuthorizationScope; import com.hubspot.singularity.SingularityRequest; import com.hubspot.singularity.SingularityRequestWithState; import com.hubspot.singularity.SingularityTaskId; import com.hubspot.singularity.SingularityUser; import com.hubspot.singularity.config.SingularityConfiguration; import com.hubspot.singularity.data.RequestManager; @Singleton public class SingularityAuthorizationHelper { private final RequestManager requestManager; private final ImmutableSet<String> adminGroups; private final ImmutableSet<String> requiredGroups; private final ImmutableSet<String> jitaGroups; private final ImmutableSet<String> defaultReadOnlyGroups; private final ImmutableSet<String> globalReadOnlyGroups; private final boolean authEnabled; @Inject public SingularityAuthorizationHelper(RequestManager requestManager, SingularityConfiguration configuration) { this.requestManager = requestManager; this.adminGroups = copyOf(configuration.getAuthConfiguration().getAdminGroups()); this.requiredGroups = copyOf(configuration.getAuthConfiguration().getRequiredGroups()); this.jitaGroups = copyOf(configuration.getAuthConfiguration().getJitaGroups()); this.defaultReadOnlyGroups = copyOf(configuration.getAuthConfiguration().getDefaultReadOnlyGroups()); this.globalReadOnlyGroups = copyOf(configuration.getAuthConfiguration().getGlobalReadOnlyGroups()); this.authEnabled = configuration.getAuthConfiguration().isEnabled(); } public static boolean groupsIntersect(Set<String> a, Set<String> b) { return !Sets.intersection(a, b).isEmpty(); } public boolean hasAdminAuthorization(Optional<SingularityUser> user) { // disabled auth == no rules! if (!authEnabled) { return true; } // not authenticated, or no groups, or no admin groups == can't possibly be admin if (!user.isPresent() || user.get().getGroups().isEmpty() || adminGroups.isEmpty()) { return false; } return groupsIntersect(user.get().getGroups(), adminGroups); } public void checkAdminAuthorization(Optional<SingularityUser> user) { if (authEnabled) { checkUnauthorized(user.isPresent(), "Please log in to perform this action."); if (!adminGroups.isEmpty()) { checkForbidden(groupsIntersect(user.get().getGroups(), adminGroups), "%s must be part of one or more admin groups: %s", user.get().getId(), JavaUtils.COMMA_JOINER.join(adminGroups)); } } } public void checkForAuthorizationByTaskId(String taskId, Optional<SingularityUser> user, SingularityAuthorizationScope scope) { if (authEnabled) { try { final SingularityTaskId taskIdObj = SingularityTaskId.valueOf(taskId); final Optional<SingularityRequestWithState> maybeRequest = requestManager.getRequest(taskIdObj.getRequestId()); if (maybeRequest.isPresent()) { checkForAuthorization(maybeRequest.get().getRequest(), user, scope); } } catch (InvalidSingularityTaskIdException e) { badRequest(e.getMessage()); } } } public void checkForAuthorizationByRequestId(String requestId, Optional<SingularityUser> user, SingularityAuthorizationScope scope) { if (authEnabled) { final Optional<SingularityRequestWithState> maybeRequest = requestManager.getRequest(requestId); if (maybeRequest.isPresent()) { checkForAuthorization(maybeRequest.get().getRequest(), user, scope); } } } public boolean isAuthorizedForRequest(SingularityRequest request, Optional<SingularityUser> user, SingularityAuthorizationScope scope) { if (!authEnabled) { return true; // no auth == no rules! } if (!user.isPresent()) { return false; } final Set<String> userGroups = user.get().getGroups(); final Set<String> readWriteGroups = Sets.union(request.getGroup().asSet(), request.getReadWriteGroups().or(Collections.<String>emptySet())); final Set<String> readOnlyGroups = request.getReadOnlyGroups().or(defaultReadOnlyGroups); final boolean userIsAdmin = !adminGroups.isEmpty() && groupsIntersect(userGroups, adminGroups); final boolean userIsJITA = !jitaGroups.isEmpty() && groupsIntersect(userGroups, jitaGroups); final boolean userIsReadWriteUser = readWriteGroups.isEmpty() || groupsIntersect(userGroups, readWriteGroups); final boolean userIsReadOnlyUser = groupsIntersect(userGroups, readOnlyGroups) || (!globalReadOnlyGroups.isEmpty() && groupsIntersect(userGroups, globalReadOnlyGroups)); final boolean userIsPartOfRequiredGroups = requiredGroups.isEmpty() || groupsIntersect(userGroups, requiredGroups); if (userIsAdmin) { return true; // Admins Rule Everything Around Me } else if (scope == SingularityAuthorizationScope.READ) { return (userIsReadOnlyUser || userIsReadWriteUser || userIsJITA) && userIsPartOfRequiredGroups; } else if (scope == SingularityAuthorizationScope.WRITE) { return (userIsReadWriteUser || userIsJITA) && userIsPartOfRequiredGroups; } else { return false; } } public void checkForAuthorization(SingularityRequest request, Optional<SingularityUser> user, SingularityAuthorizationScope scope) { if (!authEnabled) { return; } checkUnauthorized(user.isPresent(), "user must be present"); final Set<String> userGroups = user.get().getGroups(); final Set<String> readWriteGroups = Sets.union(request.getGroup().asSet(), request.getReadWriteGroups().or(Collections.<String>emptySet())); final Set<String> readOnlyGroups = request.getReadOnlyGroups().or(defaultReadOnlyGroups); final boolean userIsAdmin = !adminGroups.isEmpty() && groupsIntersect(userGroups, adminGroups); final boolean userIsJITA = !jitaGroups.isEmpty() && groupsIntersect(userGroups, jitaGroups); final boolean userIsReadWriteUser = readWriteGroups.isEmpty() || groupsIntersect(userGroups, readWriteGroups); final boolean userIsReadOnlyUser = groupsIntersect(userGroups, readOnlyGroups) || (!globalReadOnlyGroups.isEmpty() && groupsIntersect(userGroups, globalReadOnlyGroups)); final boolean userIsPartOfRequiredGroups = requiredGroups.isEmpty() || groupsIntersect(userGroups, requiredGroups); if (userIsAdmin) { return; // Admins Rule Everything Around Me } checkForbidden(userIsPartOfRequiredGroups, "%s must be a member of one or more required groups: %s", user.get().getId(), JavaUtils.COMMA_JOINER.join(requiredGroups)); if (scope == SingularityAuthorizationScope.READ) { checkForbidden(userIsReadOnlyUser || userIsReadWriteUser || userIsJITA, "%s must be a member of one or more groups to %s %s: %s", user.get().getId(), scope.name(), request.getId(), JavaUtils.COMMA_JOINER.join(Iterables.concat(readOnlyGroups, readWriteGroups, jitaGroups))); } else if (scope == SingularityAuthorizationScope.WRITE) { checkForbidden(userIsReadWriteUser || userIsJITA, "%s must be a member of one or more groups to %s %s: %s", user.get().getId(), scope.name(), request.getId(), JavaUtils.COMMA_JOINER.join(Iterables.concat(readWriteGroups, jitaGroups))); } else if (scope == SingularityAuthorizationScope.ADMIN) { checkForbidden(userIsAdmin, "%s must be a member of one or more groups to %s %s: %s", user.get().getId(), scope.name(), request.getId(), JavaUtils.COMMA_JOINER.join(adminGroups)); } } public void checkForAuthorizedChanges(SingularityRequest request, SingularityRequest oldRequest, Optional<SingularityUser> user) { if (!authEnabled) { return; } checkUnauthorized(user.isPresent(), "user must be present"); if (oldRequest.getGroup().isPresent() && !oldRequest.getReadWriteGroups().equals(request.getReadWriteGroups())) { final Set<String> userGroups = user.get().getGroups(); final boolean userIsAdmin = !adminGroups.isEmpty() && groupsIntersect(userGroups, adminGroups); final boolean userIsJITA = !jitaGroups.isEmpty() && groupsIntersect(userGroups, jitaGroups); final boolean userIsRequestOwner = userGroups.contains(oldRequest.getGroup().get()); checkUnauthorized(userIsAdmin || userIsRequestOwner || userIsJITA, "Only admins and members of the request's owner group can add or remove groups from readWriteGroups"); } } public <T> Iterable<T> filterByAuthorizedRequests(final Optional<SingularityUser> user, List<T> objects, final Function<T, String> requestIdFunction, final SingularityAuthorizationScope scope) { if (hasAdminAuthorization(user)) { return objects; } final Set<String> requestIds = copyOf(Iterables.transform(objects, new Function<T, String>() { @Override public String apply(@Nonnull T input) { return requestIdFunction.apply(input); } })); final Map<String, SingularityRequestWithState> requestMap = Maps.uniqueIndex(requestManager.getRequests(requestIds), new Function<SingularityRequestWithState, String>() { @Override public String apply(@Nonnull SingularityRequestWithState input) { return input.getRequest().getId(); } }); return Iterables.filter(objects, new Predicate<T>() { @Override public boolean apply(@Nonnull T input) { final String requestId = requestIdFunction.apply(input); return requestMap.containsKey(requestId) && isAuthorizedForRequest(requestMap.get(requestId).getRequest(), user, scope); } }); } public Iterable<String> filterAuthorizedRequestIds(final Optional<SingularityUser> user, List<String> requestIds, final SingularityAuthorizationScope scope, boolean useWebCache) { if (hasAdminAuthorization(user)) { return requestIds; } final Map<String, SingularityRequestWithState> requestMap = Maps.uniqueIndex(requestManager.getRequests(requestIds, useWebCache), new Function<SingularityRequestWithState, String>() { @Override public String apply(@Nonnull SingularityRequestWithState input) { return input.getRequest().getId(); } }); return Iterables.filter(requestIds, new Predicate<String>() { @Override public boolean apply(@Nonnull String input) { return requestMap.containsKey(input) && isAuthorizedForRequest(requestMap.get(input).getRequest(), user, scope); } }); } public String getAuthUserId(Optional<SingularityUser> user) { if (authEnabled) { checkUnauthorized(user.isPresent(), "Please log in to perform this action."); } else { checkNotFound(user.isPresent(), "Please set a user id to perform this action."); } return user.get().getId(); } }