Skip to content

Commit

Permalink
[RHCLOUD-37777] Evaluate Authz criteria when listing events for event…
Browse files Browse the repository at this point in the history
… log
  • Loading branch information
g-duval committed Feb 14, 2025
1 parent 390dcb1 commit 236e56e
Show file tree
Hide file tree
Showing 19 changed files with 636 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.redhat.cloud.notifications.auth.kessel.permission.WorkspacePermission;
import com.redhat.cloud.notifications.auth.principal.rhid.RhIdentity;
import com.redhat.cloud.notifications.config.BackendConfig;
import com.redhat.cloud.notifications.ingress.RecipientsAuthorizationCriterion;
import com.redhat.cloud.notifications.models.Endpoint;
import com.redhat.cloud.notifications.routers.SecurityContextUtil;
import io.micrometer.core.instrument.MeterRegistry;
Expand Down Expand Up @@ -166,6 +167,61 @@ public void hasPermissionOnResource(final SecurityContext securityContext, final
Log.debugf("[identity: %s][permission: %s][resource_type: %s][resource_id: %s] Permission granted", identity, resourceType, permission, resourceId);
}

/**
* Checks if the subject on the security context has permission on the
* given resource. Throws
* @param securityContext the security context to extract the subject from.
* @param authorizationCriterion the authorization criterion.
*
* @return true or false regarding if the user have access to requested resource.
*/
public boolean hasPermissionOnResource(final SecurityContext securityContext, final RecipientsAuthorizationCriterion authorizationCriterion) {
// Identify the subject.
final RhIdentity identity = SecurityContextUtil.extractRhIdentity(securityContext);
final String permission = authorizationCriterion.getRelation();
final String resourceType = authorizationCriterion.getType().toString();
final String resourceId = authorizationCriterion.getId();

// Build the request for Kessel.
final CheckRequest permissionCheckRequest = this.buildCheckRequest(identity, authorizationCriterion);

Log.tracef("[identity: %s][permission: %s][resource_type: %s][resource_id: %s] Payload for the permission check: %s", identity, permission, resourceType, resourceId, permissionCheckRequest);

// Measure the time it takes to perform the operation with Kessel.
final Timer.Sample permissionCheckTimer = Timer.start(this.meterRegistry);

// Call Kessel.
final CheckResponse response;
try {
response = this.checkClient.check(permissionCheckRequest);
} catch (final Exception e) {
Log.errorf(
e,
"[identity: %s][permission: %s][resource_type: %s][resource_id: %s] Unable to query Kessel for a permission on a resource",
identity, permission, resourceType, resourceId
);
meterRegistry.counter(KESSEL_METRICS_PERMISSION_CHECK_COUNTER_NAME, Tags.of(COUNTER_TAG_REQUEST_RESULT, COUNTER_TAG_FAILURES)).increment();
return false;
} finally {
// Stop the timer.
permissionCheckTimer.stop(this.meterRegistry.timer(KESSEL_METRICS_PERMISSION_CHECK_TIMER_NAME, Tags.of(KESSEL_METRICS_TAG_PERMISSION_KEY, permission, Constants.KESSEL_METRICS_TAG_RESOURCE_TYPE_KEY, authorizationCriterion.getType().getName())));
}

meterRegistry.counter(KESSEL_METRICS_PERMISSION_CHECK_COUNTER_NAME, Tags.of(COUNTER_TAG_REQUEST_RESULT, COUNTER_TAG_SUCCESSES)).increment();

Log.tracef("[identity: %s][permission: %s][resource_type: %s][resource_id: %s] Received payload for the permission check: %s", identity, permission, resourceType, resourceId, response);

// Verify whether the subject has permission on the resource or not.
if (response == null || CheckResponse.Allowed.ALLOWED_TRUE != response.getAllowed()) {
Log.debugf("[identity: %s][permission: %s][resource_type: %s][resource_id: %s] Permission denied", identity, permission, resourceType, resourceId);

return false;
}

Log.debugf("[identity: %s][permission: %s][resource_type: %s][resource_id: %s] Permission granted", identity, resourceType, permission, resourceId);
return true;
}

/**
* Looks up the integrations the security context's subject has the given
* permission for. Useful for when we want to "pre-filter" the integrations
Expand Down Expand Up @@ -308,6 +364,28 @@ protected CheckRequest buildCheckRequest(final RhIdentity identity, final Kessel
).build();
}

protected CheckRequest buildCheckRequest(final RhIdentity identity, final RecipientsAuthorizationCriterion recipientsAuthorizationCriterion) {
return CheckRequest.newBuilder()
.setResource(
ObjectReference.newBuilder()
.setType(ObjectType.newBuilder()
.setNamespace(recipientsAuthorizationCriterion.getType().getNamespace())
.setName(recipientsAuthorizationCriterion.getType().getName()).build())
.setId(recipientsAuthorizationCriterion.getId())
.build()
)
.setRelation(recipientsAuthorizationCriterion.getRelation())
.setSubject(
SubjectReference.newBuilder()
.setSubject(
ObjectReference.newBuilder()
.setType(ObjectType.newBuilder().setNamespace(KESSEL_RBAC_NAMESPACE).setName(KESSEL_IDENTITY_SUBJECT_TYPE).build())
.setId(getUserId(identity))
.build()
).build()
).build();
}

/**
* Build a lookup request for integrations.
* @param identity the subject's identity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class BackendConfig {
private String drawerToggle;
private String kesselInventoryToggle;
private String kesselRelationsToggle;
private String kesselChecksOnEventLogToggle;
private String maintenanceModeToggle;
private String bypassBehaviorGroupMaxCreationLimitToggle;

Expand Down Expand Up @@ -114,6 +115,7 @@ void postConstruct() {
drawerToggle = toggleRegistry.register("drawer", true);
kesselInventoryToggle = toggleRegistry.register("kessel-inventory", true);
kesselRelationsToggle = toggleRegistry.register("kessel-relations", true);
kesselChecksOnEventLogToggle = toggleRegistry.register("kessel-checks-on-event-log", true);
maintenanceModeToggle = toggleRegistry.register("notifications-maintenance-mode", true);
bypassBehaviorGroupMaxCreationLimitToggle = toggleRegistry.register("bypass-behavior-group-max-creation-limit", true);
}
Expand Down Expand Up @@ -208,6 +210,15 @@ public boolean isKesselRelationsEnabled(String orgId) {
}
}

public boolean isKesselChecksOnEventLogEnabled(String orgId) {
if (unleashEnabled) {
UnleashContext unleashContext = buildUnleashContextWithOrgId(orgId);
return unleash.isEnabled(kesselChecksOnEventLogToggle, unleashContext, false);
} else {
return false;
}
}

public String getKesselDomain() {
return kesselDomain;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.redhat.cloud.notifications.models.EndpointType;
import com.redhat.cloud.notifications.models.Event;
import com.redhat.cloud.notifications.models.NotificationStatus;
import com.redhat.cloud.notifications.routers.handlers.event.EventAuthorizationCriterion;
import com.redhat.cloud.notifications.utils.RecipientsAuthorizationCriterionExtractor;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
Expand All @@ -25,13 +27,38 @@ public class EventRepository {
@Inject
EntityManager entityManager;

@Inject
RecipientsAuthorizationCriterionExtractor recipientsAuthorizationCriterionExtractor;

public List<EventAuthorizationCriterion> getEventsWithCriterion(String orgId, Set<UUID> bundleIds, Set<UUID> appIds, String eventTypeDisplayName,
LocalDate startDate, LocalDate endDate, Set<EndpointType> endpointTypes, Set<CompositeEndpointType> compositeEndpointTypes,
Set<Boolean> invocationResults, Set<NotificationStatus> status) {

String hql = "FROM Event e WHERE e.orgId = :orgId";

hql = addHqlConditions(hql, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, Optional.empty(), true);
// we are looking for events with auth criterion only
hql += " AND e.hasAuthorizationCriterion is true";

TypedQuery<Event> typedQuery = entityManager.createQuery(hql, Event.class);
setQueryParams(typedQuery, orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, Optional.empty());

List<Event> eventsWithAuthorizationCriterion = typedQuery.getResultList();
List<EventAuthorizationCriterion> eventAuthorizationCriterion = new ArrayList<>();
for (Event event : eventsWithAuthorizationCriterion) {
eventAuthorizationCriterion.add(new EventAuthorizationCriterion(event.getId(), recipientsAuthorizationCriterionExtractor.extract(event)));
}
return eventAuthorizationCriterion;
}

public List<Event> getEvents(String orgId, Set<UUID> bundleIds, Set<UUID> appIds, String eventTypeDisplayName,
LocalDate startDate, LocalDate endDate, Set<EndpointType> endpointTypes, Set<CompositeEndpointType> compositeEndpointTypes,
Set<Boolean> invocationResults, boolean fetchNotificationHistory, Set<NotificationStatus> status, Query query) {
Set<Boolean> invocationResults, boolean fetchNotificationHistory, Set<NotificationStatus> status, Query query,
Optional<List<UUID>> uuidToExclude, boolean includeEventsWithAuthCriterion) {
query.setSortFields(Event.SORT_FIELDS);
query.setDefaultSortBy("created:DESC");
Optional<Query.Sort> sort = query.getSort();
List<UUID> eventIds = getEventIds(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, query);
List<UUID> eventIds = getEventIds(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, query, uuidToExclude, includeEventsWithAuthCriterion);
if (eventIds.isEmpty()) {
return new ArrayList<>();
}
Expand All @@ -55,13 +82,13 @@ public List<Event> getEvents(String orgId, Set<UUID> bundleIds, Set<UUID> appIds
public Long count(String orgId, Set<UUID> bundleIds, Set<UUID> appIds, String eventTypeDisplayName,
LocalDate startDate, LocalDate endDate, Set<EndpointType> endpointTypes,
Set<CompositeEndpointType> compositeEndpointTypes, Set<Boolean> invocationResults,
Set<NotificationStatus> status) {
Set<NotificationStatus> status, Optional<List<UUID>> uuidToExclude, Boolean includeEventsWithAuthCriterion) {
String hql = "SELECT COUNT(*) FROM Event e WHERE e.orgId = :orgId";

hql = addHqlConditions(hql, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status);
hql = addHqlConditions(hql, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, uuidToExclude, includeEventsWithAuthCriterion);

TypedQuery<Long> query = entityManager.createQuery(hql, Long.class);
setQueryParams(query, orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status);
setQueryParams(query, orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, uuidToExclude);

return query.getSingleResult();
}
Expand All @@ -76,18 +103,18 @@ private String getOrderBy(Query.Sort sort) {

private List<UUID> getEventIds(String orgId, Set<UUID> bundleIds, Set<UUID> appIds, String eventTypeDisplayName,
LocalDate startDate, LocalDate endDate, Set<EndpointType> endpointTypes, Set<CompositeEndpointType> compositeEndpointTypes,
Set<Boolean> invocationResults, Set<NotificationStatus> status, Query query) {
Set<Boolean> invocationResults, Set<NotificationStatus> status, Query query, Optional<List<UUID>> uuidToExclude, boolean includeEventsWithAuthCriterion) {
String hql = "SELECT e.id FROM Event e WHERE e.orgId = :orgId";

hql = addHqlConditions(hql, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status);
hql = addHqlConditions(hql, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, uuidToExclude, includeEventsWithAuthCriterion);
Optional<Query.Sort> sort = query.getSort();

if (sort.isPresent()) {
hql += getOrderBy(sort.get());
}

TypedQuery<UUID> typedQuery = entityManager.createQuery(hql, UUID.class);
setQueryParams(typedQuery, orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status);
setQueryParams(typedQuery, orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, endpointTypes, compositeEndpointTypes, invocationResults, status, uuidToExclude);

Query.Limit limit = query.getLimit();

Expand All @@ -100,10 +127,13 @@ private List<UUID> getEventIds(String orgId, Set<UUID> bundleIds, Set<UUID> appI
private static String addHqlConditions(String hql, Set<UUID> bundleIds, Set<UUID> appIds, String eventTypeDisplayName,
LocalDate startDate, LocalDate endDate, Set<EndpointType> endpointTypes,
Set<CompositeEndpointType> compositeEndpointTypes, Set<Boolean> invocationResults,
Set<NotificationStatus> status) {
Set<NotificationStatus> status, Optional<List<UUID>> uuidToExclude, boolean includeEventsWithAuthCriterion) {

List<String> bundleOrAppsConditions = new ArrayList<>();

if (uuidToExclude.isPresent()) {
bundleOrAppsConditions.add("e.id NOT IN (:uuidToExclude)");
}
if (bundleIds != null && !bundleIds.isEmpty()) {
bundleOrAppsConditions.add("e.bundleId IN (:bundleIds)");
}
Expand All @@ -126,6 +156,10 @@ private static String addHqlConditions(String hql, Set<UUID> bundleIds, Set<UUID
hql += " AND e.created <= :endDate";
}

if (!includeEventsWithAuthCriterion) {
hql += " AND e.hasAuthorizationCriterion is false";
}

boolean checkEndpointType = (endpointTypes != null && !endpointTypes.isEmpty()) || (compositeEndpointTypes != null && !compositeEndpointTypes.isEmpty());
boolean checkInvocationResult = invocationResults != null && !invocationResults.isEmpty();
boolean checkStatus = status != null && !status.isEmpty();
Expand Down Expand Up @@ -161,8 +195,11 @@ private static String addHqlConditions(String hql, Set<UUID> bundleIds, Set<UUID

private void setQueryParams(TypedQuery<?> query, String orgId, Set<UUID> bundleIds, Set<UUID> appIds, String eventTypeName,
LocalDate startDate, LocalDate endDate, Set<EndpointType> endpointTypes, Set<CompositeEndpointType> compositeEndpointTypes,
Set<Boolean> invocationResults, Set<NotificationStatus> status) {
Set<Boolean> invocationResults, Set<NotificationStatus> status, Optional<List<UUID>> uuidToExclude) {
query.setParameter("orgId", orgId);
if (uuidToExclude.isPresent()) {
query.setParameter("uuidToExclude", uuidToExclude.get());
}
if (bundleIds != null && !bundleIds.isEmpty()) {
query.setParameter("bundleIds", bundleIds);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.redhat.cloud.notifications.routers.handlers.event;

import com.redhat.cloud.notifications.ingress.RecipientsAuthorizationCriterion;
import java.util.UUID;

public record EventAuthorizationCriterion(
UUID id,
RecipientsAuthorizationCriterion authorizationCriterion
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -113,8 +114,27 @@ public Page<EventLogEntry> getInternalEvents(final SecurityContext securityConte
}

String orgId = getOrgId(securityContext);
List<Event> events = eventRepository.getEvents(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, basicTypes, compositeTypes, invocationResults, includeActions, notificationStatusSet, query);

List<Event> events;
Long count;
if (backendConfig.isKesselChecksOnEventLogEnabled(orgId)) {
Log.info("Check for events with authorization criterion");
List<EventAuthorizationCriterion> listEventsAuthCriterion = eventRepository.getEventsWithCriterion(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, basicTypes, compositeTypes, invocationResults, notificationStatusSet);
List<UUID> uuidToExclude = new ArrayList<>();
for (EventAuthorizationCriterion eventAuthorizationCriterion : listEventsAuthCriterion) {
if (!kesselAuthorization.hasPermissionOnResource(securityContext, eventAuthorizationCriterion.authorizationCriterion())) {
Log.infof("%s is not visible for current user", eventAuthorizationCriterion.id());
uuidToExclude.add(eventAuthorizationCriterion.id());
}
}
if (uuidToExclude.isEmpty()) {
uuidToExclude = null;
}
events = eventRepository.getEvents(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, basicTypes, compositeTypes, invocationResults, includeActions, notificationStatusSet, query, Optional.ofNullable(uuidToExclude), true);
count = eventRepository.count(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, basicTypes, compositeTypes, invocationResults, notificationStatusSet, Optional.ofNullable(uuidToExclude), true);
} else {
events = eventRepository.getEvents(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, basicTypes, compositeTypes, invocationResults, includeActions, notificationStatusSet, query, Optional.empty(), false);
count = eventRepository.count(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, basicTypes, compositeTypes, invocationResults, notificationStatusSet, Optional.empty(), false);
}
if (events.isEmpty()) {
Meta meta = new Meta();
meta.setCount(0L);
Expand Down Expand Up @@ -160,7 +180,6 @@ public Page<EventLogEntry> getInternalEvents(final SecurityContext securityConte
}
return entry;
}).collect(Collectors.toList());
Long count = eventRepository.count(orgId, bundleIds, appIds, eventTypeDisplayName, startDate, endDate, basicTypes, compositeTypes, invocationResults, notificationStatusSet);

Meta meta = new Meta();
meta.setCount(count);
Expand Down
Loading

0 comments on commit 236e56e

Please sign in to comment.