org.graylog.security.shares.EntitySharesService Maven / Gradle / Ivy
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* .
*/
package org.graylog.security.shares;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import org.apache.shiro.subject.Subject;
import org.graylog.grn.GRN;
import org.graylog.grn.GRNRegistry;
import org.graylog.security.BuiltinCapabilities;
import org.graylog.security.Capability;
import org.graylog.security.DBGrantService;
import org.graylog.security.GrantDTO;
import org.graylog.security.entities.EntityDependencyPermissionChecker;
import org.graylog.security.entities.EntityDependencyResolver;
import org.graylog.security.entities.EntityDescriptor;
import org.graylog.security.events.EntitySharesUpdateEvent;
import org.graylog.security.shares.EntityShareResponse.ActiveShare;
import org.graylog.security.shares.EntityShareResponse.AvailableCapability;
import org.graylog2.plugin.database.users.User;
import org.graylog2.plugin.rest.ValidationResult;
import javax.inject.Inject;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
/**
* Handler for sharing calls.
*/
public class EntitySharesService {
private final DBGrantService grantService;
private final EntityDependencyResolver entityDependencyResolver;
private final EntityDependencyPermissionChecker entityDependencyPermissionChecker;
private final GRNRegistry grnRegistry;
private final GranteeService granteeService;
private final EventBus serverEventBus;
@Inject
public EntitySharesService(DBGrantService grantService,
EntityDependencyResolver entityDependencyResolver,
EntityDependencyPermissionChecker entityDependencyPermissionChecker,
GRNRegistry grnRegistry,
GranteeService granteeService,
EventBus serverEventBus) {
this.grantService = grantService;
this.entityDependencyResolver = entityDependencyResolver;
this.entityDependencyPermissionChecker = entityDependencyPermissionChecker;
this.grnRegistry = grnRegistry;
this.granteeService = granteeService;
this.serverEventBus = serverEventBus;
}
/**
* Prepares the sharing operation by running some checks and returning available capabilities and grantees
* as well as active shares and information about missing dependencies.
*
* @param ownedEntity the entity that should be shared and is owned by the sharing user
* @param request sharing request
* @param sharingUser the sharing user
* @param sharingSubject the sharing subject
* @return the response
*/
public EntityShareResponse prepareShare(GRN ownedEntity,
EntityShareRequest request,
User sharingUser,
Subject sharingSubject) {
requireNonNull(ownedEntity, "ownedEntity cannot be null");
requireNonNull(request, "request cannot be null");
requireNonNull(sharingUser, "sharingUser cannot be null");
requireNonNull(sharingSubject, "sharingSubject cannot be null");
final GRN sharingUserGRN = grnRegistry.ofUser(sharingUser);
final Set modifiableGrantees = getModifiableGrantees(sharingUser, sharingUserGRN, ownedEntity);
final Set modifiableGranteeGRNs = modifiableGrantees.stream().map(Grantee::grn).collect(Collectors.toSet());
final ImmutableSet modifiableActiveShares = getActiveShares(ownedEntity, sharingUser, modifiableGranteeGRNs);
return EntityShareResponse.builder()
.entity(ownedEntity.toString())
.sharingUser(sharingUserGRN)
.availableGrantees(modifiableGrantees)
.availableCapabilities(getAvailableCapabilities())
.activeShares(modifiableActiveShares)
.selectedGranteeCapabilities(getSelectedGranteeCapabilities(modifiableActiveShares, request))
.missingPermissionsOnDependencies(checkMissingPermissionsOnDependencies(ownedEntity, sharingUserGRN, modifiableActiveShares, request))
.validationResult(validateRequest(ownedEntity, request, sharingUser, modifiableGranteeGRNs))
.build();
}
private Set getModifiableGrantees(User sharingUser, GRN sharingUserGRN, GRN ownedEntity) {
final Set availableGrantees = granteeService.getAvailableGrantees(sharingUser);
final Set availableGranteeGRNs = availableGrantees.stream().map(Grantee::grn).collect(Collectors.toSet());
final ImmutableSet activeShares = getActiveShares(ownedEntity, sharingUser, availableGranteeGRNs);
return granteeService.getModifiableGrantees(availableGrantees, activeShares);
}
/**
* Share / unshare an entity with one or more grantees.
* The grants in the request are created or, if they already exist, updated.
*
* @param ownedEntity the target entity for the updated grants
* @param request the request containing grantees and their capabilities
* @param sharingUser the user executing the request
*/
public EntityShareResponse updateEntityShares(GRN ownedEntity, EntityShareRequest request, User sharingUser) {
requireNonNull(ownedEntity, "ownedEntity cannot be null");
requireNonNull(request, "request cannot be null");
requireNonNull(sharingUser, "sharingUser cannot be null");
final ImmutableMap selectedGranteeCapabilities = request.selectedGranteeCapabilities()
.orElse(ImmutableMap.of());
final String userName = sharingUser.getName();
final GRN sharingUserGRN = grnRegistry.ofUser(sharingUser);
final Set modifiableGrantees = getModifiableGrantees(sharingUser, sharingUserGRN, ownedEntity);
final Set modifiableGranteeGRNs = modifiableGrantees.stream().map(Grantee::grn).collect(Collectors.toSet());
final List existingGrants = grantService.getForTargetExcludingGrantee(ownedEntity, sharingUserGRN);
existingGrants.removeIf(grant -> !modifiableGranteeGRNs.contains(grant.grantee()));
final EntityShareResponse.Builder responseBuilder = EntityShareResponse.builder()
.entity(ownedEntity.toString())
.sharingUser(sharingUserGRN)
.availableGrantees(modifiableGrantees)
.availableCapabilities(getAvailableCapabilities())
.missingPermissionsOnDependencies(checkMissingPermissionsOnDependencies(ownedEntity, sharingUserGRN, ImmutableSet.of(), request));
final EntitySharesUpdateEvent.Builder updateEventBuilder = EntitySharesUpdateEvent.builder()
.user(sharingUser)
.entity(ownedEntity);
// Abort if validation fails, but try to return a complete EntityShareResponse
final ValidationResult validationResult = validateRequest(ownedEntity, request, sharingUser, modifiableGranteeGRNs);
if (validationResult.failed()) {
final ImmutableSet activeShares = getActiveShares(ownedEntity, sharingUser, modifiableGranteeGRNs);
return responseBuilder
.activeShares(activeShares)
.selectedGranteeCapabilities(getSelectedGranteeCapabilities(activeShares, request))
.validationResult(validationResult)
.build();
}
// Update capabilities of existing grants (for a grantee)
existingGrants.stream().filter(grantDTO -> request.grantees().contains(grantDTO.grantee())).forEach((g -> {
final Capability newCapability = selectedGranteeCapabilities.get(g.grantee());
if (!g.capability().equals(newCapability)) {
grantService.save(g.toBuilder()
.capability(newCapability)
.updatedBy(userName)
.updatedAt(ZonedDateTime.now(ZoneOffset.UTC))
.build());
updateEventBuilder.addUpdates(g.grantee(), newCapability, g.capability());
}
}));
// Create newly added grants
// TODO Create multiple entries with one db query
selectedGranteeCapabilities.forEach((grantee, capability) -> {
if (existingGrants.stream().noneMatch(eg -> eg.grantee().equals(grantee))) {
grantService.create(GrantDTO.builder()
.grantee(grantee)
.capability(capability)
.target(ownedEntity)
.build(),
sharingUser);
updateEventBuilder.addCreates(grantee, capability);
}
});
// remove grants that are not present anymore
// TODO delete multiple entries with one db query
existingGrants.forEach((g) -> {
if (!selectedGranteeCapabilities.containsKey(g.grantee())) {
grantService.delete(g.id());
updateEventBuilder.addDeletes(g.grantee(), g.capability());
}
});
postUpdateEvent(updateEventBuilder.build());
final ImmutableSet activeShares = getActiveShares(ownedEntity, sharingUser, modifiableGranteeGRNs);
return responseBuilder
.activeShares(activeShares)
.selectedGranteeCapabilities(getSelectedGranteeCapabilities(activeShares, request))
.build();
}
private void postUpdateEvent(EntitySharesUpdateEvent updateEvent) {
this.serverEventBus.post(updateEvent);
}
private ValidationResult validateRequest(GRN ownedEntity, EntityShareRequest request, User sharingUser, Set availableGranteeGRNs) {
final ValidationResult validationResult = new ValidationResult();
final List allEntityGrants = grantService.getForTarget(ownedEntity);
final List existingGrants = grantService.getForTargetExcludingGrantee(ownedEntity, grnRegistry.ofUser(sharingUser));
// The initial request doesn't submit a grantee selection. Just return.
if (!request.selectedGranteeCapabilities().isPresent()) {
return validationResult;
}
final ImmutableMap selectedGranteeCapabilities = request.selectedGranteeCapabilities().get();
// If there is still an owner in the selection, everything is fine
if (selectedGranteeCapabilities.containsValue(Capability.OWN)) {
return validationResult;
}
// If this entity is already ownerless, things can't get any worse. Let this request pass.
if (allEntityGrants.stream().noneMatch(g -> g.capability().equals(Capability.OWN))) {
return validationResult;
}
// Iterate over all existing owner grants and find modifications
ArrayList removedOwners = new ArrayList<>();
existingGrants.stream().filter(g -> g.capability().equals(Capability.OWN)).forEach((g) -> {
// owner got removed
if (!selectedGranteeCapabilities.containsKey(g.grantee())) {
// Ignore owners that were invisible to the requesting user
if (availableGranteeGRNs.contains(g.grantee())) {
removedOwners.add(g.grantee());
}
// owner capability got changed
} else if (!selectedGranteeCapabilities.get(g.grantee()).equals(Capability.OWN)) {
removedOwners.add(g.grantee());
}
});
// If all removedOwners are applied, is there still at least one owner left?
if (allEntityGrants.stream().filter(g -> g.capability().equals(Capability.OWN))
.map(GrantDTO::grantee).anyMatch(grantee -> !removedOwners.contains(grantee))) {
return validationResult;
}
validationResult.addError(EntityShareRequest.SELECTED_GRANTEE_CAPABILITIES,
String.format(Locale.US, "Removing the following owners <%s> will leave the entity ownerless.", removedOwners));
// Also return the grantees as list to be used by the frontend
validationResult.addContext(EntityShareRequest.SELECTED_GRANTEE_CAPABILITIES,
removedOwners.stream().map(Objects::toString).collect(Collectors.toSet()));
return validationResult;
}
private Map getSelectedGranteeCapabilities(ImmutableSet activeShares, EntityShareRequest shareRequest) {
// If the user doesn't submit a grantee selection we return the active shares as selection so the frontend
// can just render it
if (!shareRequest.selectedGranteeCapabilities().isPresent()) {
return activeShares.stream()
.collect(Collectors.toMap(ActiveShare::grantee, ActiveShare::capability));
}
// If the user submits a grantee selection, we only return that one because we expect the frontend to always
// submit the full selection not only added/removed grantees. If the grantee selection is empty, that means
// all shares should be removed.
return shareRequest.selectedGranteeCapabilities().get();
}
private ImmutableSet getActiveShares(GRN ownedEntity, User sharingUser, Set availableGranteeGRNs) {
final List activeGrants = grantService.getForTargetExcludingGrantee(ownedEntity, grnRegistry.ofUser(sharingUser));
return activeGrants.stream()
.filter(grant -> availableGranteeGRNs.contains(grant.grantee()))
.map(grant -> ActiveShare.create(grnRegistry.newGRN("grant", grant.id()).toString(), grant.grantee(), grant.capability()))
.collect(ImmutableSet.toImmutableSet());
}
private ImmutableSet getAvailableCapabilities() {
// TODO: Don't use GRNs for capabilities
return BuiltinCapabilities.allSharingCapabilities().stream()
.map(descriptor -> EntityShareResponse.AvailableCapability.create(descriptor.capability().toId(), descriptor.title()))
.collect(ImmutableSet.toImmutableSet());
}
private ImmutableMap> checkMissingPermissionsOnDependencies(GRN entity, GRN sharingUser, ImmutableSet activeShares, EntityShareRequest shareRequest) {
// In the initial request, the user doesn't submit a grantee selection
// We need to use the active shares to check the dependency permissions
Set selectedGrantees;
if (!shareRequest.selectedGranteeCapabilities().isPresent()) {
selectedGrantees = activeShares.stream().map(ActiveShare::grantee).collect(Collectors.toSet());
} else {
selectedGrantees = shareRequest.selectedGranteeCapabilities()
.orElse(ImmutableMap.of()).keySet();
}
final ImmutableSet dependencies = entityDependencyResolver.resolve(entity);
return entityDependencyPermissionChecker.check(sharingUser, dependencies, selectedGrantees).asMap();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy