Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add entity groups #19574

Draft
wants to merge 38 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3453a21
Move categories to open repo
ryan-carroll-graylog Jun 4, 2024
d114582
Remove asset specific rest objects
ryan-carroll-graylog Jun 4, 2024
d3004df
Add changelog
ryan-carroll-graylog Jun 4, 2024
e2095b8
Add license headers
ryan-carroll-graylog Jun 4, 2024
1681a06
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 4, 2024
9197aa2
Add audit logging
ryan-carroll-graylog Jun 4, 2024
7512c2c
Add missing APIRoutes file
ryan-carroll-graylog Jun 4, 2024
600bf5c
Fix failing test
ryan-carroll-graylog Jun 4, 2024
fcad640
Migrate to entity grouping model
ryan-carroll-graylog Jun 5, 2024
e829652
Refactor
ryan-carroll-graylog Jun 6, 2024
3ee5930
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 6, 2024
a8981c3
Fill in implementation gaps
ryan-carroll-graylog Jun 6, 2024
c6e7854
Fix changelog, license headers, cleanup
ryan-carroll-graylog Jun 6, 2024
0a6d82e
Remove API routes changes
ryan-carroll-graylog Jun 6, 2024
7cfeabb
Cleanup
ryan-carroll-graylog Jun 6, 2024
1272f95
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 6, 2024
cc34501
Use string as type key, add bulk entity ID search EP
ryan-carroll-graylog Jun 7, 2024
d2b5b91
Add license headers
ryan-carroll-graylog Jun 7, 2024
46c77af
Add NoAudit annotation for test failure
ryan-carroll-graylog Jun 7, 2024
71a915d
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 7, 2024
e53295c
Move files
ryan-carroll-graylog Jun 7, 2024
6c79435
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 10, 2024
154b406
Address PR feedback: var names, duplicates when updating
ryan-carroll-graylog Jun 10, 2024
8f994b6
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 12, 2024
be51565
Fix indent
ryan-carroll-graylog Jun 12, 2024
3f40a74
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 13, 2024
c0c0448
Add feature flag
ryan-carroll-graylog Jun 13, 2024
04b68ac
Address PR feedback
ryan-carroll-graylog Jun 14, 2024
7d0054d
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 14, 2024
4b7bb14
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 20, 2024
e4d5418
Adress PR feedback
ryan-carroll-graylog Jun 20, 2024
4706778
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 21, 2024
1e1e1bf
Update audit logging, PR feedback
ryan-carroll-graylog Jun 21, 2024
031bb1b
Add license header, PR feedback
ryan-carroll-graylog Jun 21, 2024
4a2841c
Add group audit type to audit test
ryan-carroll-graylog Jun 21, 2024
ec0ce33
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jun 24, 2024
f20815e
Merge branch 'master' into feature/add-categories
ryan-carroll-graylog Jul 8, 2024
0a8936a
Create categories page (#19852)
simonychuang Jul 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/pr-19547.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "a"
message = "Added entity groups feature."

issues = ["graylog-plugin-enterprise#7385"]
pulls = ["19547"]
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ public class AuditEventTypes implements PluginAuditEventTypes {
public static final String TELEMETRY_USER_SETTINGS_UPDATE = PREFIX + "telemetry_user_settings:update";
public static final String CONTENT_STREAM_USER_SETTINGS_UPDATE = PREFIX + "content_stream_user_settings:update";

public static final String ENTITY_GROUP_CREATE = PREFIX + "category:create";
public static final String ENTITY_GROUP_UPDATE = PREFIX + "category:update";
public static final String ENTITY_GROUP_DELETE = PREFIX + "category:delete";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might have been missed in the recent changes, but I believe we were going to use the name entity_group everywhere.

Suggested change
public static final String ENTITY_GROUP_CREATE = PREFIX + "category:create";
public static final String ENTITY_GROUP_UPDATE = PREFIX + "category:update";
public static final String ENTITY_GROUP_DELETE = PREFIX + "category:delete";
public static final String ENTITY_GROUP_CREATE = PREFIX + "entity_group:create";
public static final String ENTITY_GROUP_UPDATE = PREFIX + "entity_group:update";
public static final String ENTITY_GROUP_DELETE = PREFIX + "entity_group:delete";


private static final ImmutableSet<String> EVENT_TYPES = ImmutableSet.<String>builder()
.add(ALARM_CALLBACK_CREATE)
.add(ALARM_CALLBACK_DELETE)
Expand Down Expand Up @@ -309,6 +313,9 @@ public class AuditEventTypes implements PluginAuditEventTypes {
.add(TELEMETRY_USER_SETTINGS_UPDATE)
.add(CONTENT_STREAM_USER_SETTINGS_UPDATE)
.add(CERTIFICATE_RENEWAL_MANUALLY_INITIATED)
.add(ENTITY_GROUP_CREATE)
.add(ENTITY_GROUP_UPDATE)
.add(ENTITY_GROUP_DELETE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like formatters for these audit entries are still needed, since the general message is still being logged.
image

.build();

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.entitygroups;

import org.graylog2.entitygroups.model.EntityGroup;
import org.graylog2.entitygroups.model.DBEntityGroupService;
import org.graylog2.database.PaginatedList;
import org.graylog2.entitygroups.model.EntityType;
import org.graylog2.rest.models.SortOrder;

import jakarta.inject.Inject;

import jakarta.ws.rs.NotFoundException;

import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

public class EntityGroupService {
private final DBEntityGroupService dbEntityGroupService;

@Inject
public EntityGroupService(DBEntityGroupService dbEntityGroupService) {
this.dbEntityGroupService = dbEntityGroupService;
}

public PaginatedList<EntityGroup> findPaginated(String query, int page, int perPage, SortOrder order,
String sortByField, Predicate<EntityGroup> filter) {

return dbEntityGroupService.findPaginated(query, page, perPage, order.toBsonSort(sortByField), filter);
}

public Optional<EntityGroup> getByName(String groupName) {
return dbEntityGroupService.getByName(groupName);
}

public List<EntityGroup> getAllForEntity(EntityType type, String entityId) {
return dbEntityGroupService.getAllForEntity(type, entityId);
}

public EntityGroup create(EntityGroup group) {
return dbEntityGroupService.save(group);
}

public EntityGroup update(String id, EntityGroup group) {
if (dbEntityGroupService.get(id).isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below, but even if not, this should use the same code as addEntityToGroup (but in reality it should simply perform an update on the existing document and not use load-modify-store at all).

throw new NotFoundException("Unable to find entity group to update");
}
return dbEntityGroupService.save(group);

}

public EntityGroup addEntityToGroup(String groupId, EntityType type, String entityId) {
final EntityGroup group = requireEntityGroup(groupId);
return dbEntityGroupService.save(group.addEntity(type, entityId));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be using an atomic update instead of a load-modify-store pattern.
The issue here is that updating a group this way is not atomic and can easily cause updates that happen concurrently to be lost.

If the database query tries to update a document that doesn't exist (which should only happen if a group is concurrently deleted or someone sends arbitrary IDs in the request) the update query should simply fail. Thus we would save one query by not having to load the entire entity.

}

public long delete(String id) {
return dbEntityGroupService.delete(id);
}

public EntityGroup requireEntityGroup(String id) {
return dbEntityGroupService.get(id)
.orElseThrow(() -> new IllegalArgumentException("Unable to find entity group to update"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.entitygroups.contentpacks;

import org.graylog2.contentpacks.EntityDescriptorIds;
import org.graylog2.contentpacks.facades.EntityFacade;
import org.graylog2.contentpacks.model.ModelType;
import org.graylog2.contentpacks.model.entities.Entity;
import org.graylog2.contentpacks.model.entities.EntityDescriptor;
import org.graylog2.contentpacks.model.entities.EntityExcerpt;
import org.graylog2.contentpacks.model.entities.NativeEntity;
import org.graylog2.contentpacks.model.entities.NativeEntityDescriptor;
import org.graylog2.contentpacks.model.entities.references.ValueReference;
import org.graylog2.entitygroups.model.EntityGroup;
import org.graylog2.plugin.indexer.searches.timeranges.InvalidRangeParametersException;

import java.util.Map;
import java.util.Optional;
import java.util.Set;

// TODO: Implement methods
public class EntityGroupFacade implements EntityFacade<EntityGroup> {
public static final ModelType TYPE_V1 = ModelType.of("entity_group", "1");

@Override
public Optional<Entity> exportEntity(EntityDescriptor entityDescriptor, EntityDescriptorIds entityDescriptorIds) {
return Optional.empty();
}

@Override
public NativeEntity<EntityGroup> createNativeEntity(Entity entity, Map<String, ValueReference> parameters, Map<EntityDescriptor, Object> nativeEntities, String username) throws InvalidRangeParametersException {
return null;
}

@Override
public Optional<NativeEntity<EntityGroup>> loadNativeEntity(NativeEntityDescriptor nativeEntityDescriptor) {
return Optional.empty();
}

@Override
public void delete(EntityGroup nativeEntity) {

}

@Override
public EntityExcerpt createExcerpt(EntityGroup nativeEntity) {
return null;
}

@Override
public Set<EntityExcerpt> listEntityExcerpts() {
return Set.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.entitygroups.contentpacks.entities;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.auto.value.AutoValue;
import com.google.common.graph.MutableGraph;
import org.graylog2.contentpacks.NativeEntityConverter;
import org.graylog2.contentpacks.model.entities.Entity;
import org.graylog2.contentpacks.model.entities.EntityDescriptor;
import org.graylog2.contentpacks.model.entities.EntityV1;
import org.graylog2.contentpacks.model.entities.ScopedContentPackEntity;
import org.graylog2.contentpacks.model.entities.references.ValueReference;
import org.graylog2.entitygroups.model.EntityGroup;
import org.graylog2.entitygroups.model.EntityType;

import java.util.List;
import java.util.Map;

import static org.graylog2.entitygroups.model.EntityGroup.FIELD_ENTITIES;
import static org.graylog2.entitygroups.model.EntityGroup.FIELD_NAME;

@JsonAutoDetect
@AutoValue
@JsonDeserialize(builder = EntityGroupEntity.Builder.class)
public abstract class EntityGroupEntity extends ScopedContentPackEntity implements NativeEntityConverter<EntityGroup> {
@JsonProperty(FIELD_NAME)
public abstract String name();

@JsonProperty(FIELD_ENTITIES)
public abstract Map<EntityType, List<String>> entities();

public static Builder builder() {
return Builder.create();
}

public abstract Builder toBuilder();

@AutoValue.Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract static class Builder extends ScopedContentPackEntity.AbstractBuilder<Builder> {
@JsonProperty(FIELD_NAME)
public abstract Builder name(String name);

@JsonProperty(FIELD_ENTITIES)
public abstract Builder entities(Map<EntityType, List<String>> entities);

public abstract EntityGroupEntity build();

@JsonCreator
public static Builder create() {
return new AutoValue_EntityGroupEntity.Builder();
}
}

@Override
public EntityGroup toNativeEntity(Map<String, ValueReference> parameters,
Map<EntityDescriptor, Object> nativeEntities) {
// TODO: Need to convert content pack IDs to DB object IDs for the entities map here.
return null;
}

@Override
public void resolveForInstallation(EntityV1 entity,
Map<String, ValueReference> parameters,
Map<EntityDescriptor, Entity> entities,
MutableGraph<Entity> graph) {
// TODO: Need to flag all entity dependencies in the entities map so that they get installed first.
// TODO: They will need to exist in the DB with IDs we can reference once this `toNativeEntity` is called.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.entitygroups.model;

import com.google.common.collect.ImmutableMap;
import com.mongodb.BasicDBObject;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.CollationStrength;
import com.mongodb.client.model.IndexOptions;
import org.bson.conversions.Bson;
import org.graylog2.database.MongoCollections;
import org.graylog2.database.PaginatedList;
import org.graylog2.database.entities.EntityScopeService;
import org.graylog2.database.pagination.MongoPaginationHelper;
import org.graylog2.database.utils.MongoUtils;
import org.graylog2.database.utils.ScopedEntityMongoUtils;
import org.graylog2.search.SearchQuery;
import org.graylog2.search.SearchQueryField;
import org.graylog2.search.SearchQueryParser;

import jakarta.inject.Inject;

import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

import static com.mongodb.client.model.Filters.and;
import static com.mongodb.client.model.Filters.eq;
import static com.mongodb.client.model.Filters.exists;
import static com.mongodb.client.model.Filters.in;

public class DBEntityGroupService {
public static final String COLLECTION_NAME = "entity_groups";

private static final ImmutableMap<String, SearchQueryField> ALLOWED_FIELDS = ImmutableMap.<String, SearchQueryField>builder()
.put(EntityGroup.FIELD_NAME, SearchQueryField.create(EntityGroup.FIELD_NAME))
.build();

private final MongoCollection<EntityGroup> collection;
private final MongoUtils<EntityGroup> mongoUtils;
private final ScopedEntityMongoUtils<EntityGroup> scopedEntityMongoUtils;
private final MongoPaginationHelper<EntityGroup> paginationHelper;
private final SearchQueryParser searchQueryParser;

@Inject
public DBEntityGroupService(MongoCollections mongoCollections,
EntityScopeService entityScopeService) {
this.collection = mongoCollections.collection(COLLECTION_NAME, EntityGroup.class);
this.mongoUtils = mongoCollections.utils(collection);
this.scopedEntityMongoUtils = mongoCollections.scopedEntityUtils(collection, entityScopeService);
this.paginationHelper = mongoCollections.paginationHelper(collection);
this.searchQueryParser = new SearchQueryParser(EntityGroup.FIELD_ENTITIES, ALLOWED_FIELDS);

final IndexOptions caseInsensitiveOptions = new IndexOptions()
.collation(Collation.builder().locale("en").collationStrength(CollationStrength.SECONDARY).build())
.unique(true);
collection.createIndex(new BasicDBObject(EntityGroup.FIELD_NAME, 1), caseInsensitiveOptions);
kroepke marked this conversation as resolved.
Show resolved Hide resolved
}

public Optional<EntityGroup> get(String id) {
return mongoUtils.getById(id);
}

public PaginatedList<EntityGroup> findPaginated(String query, int page, int perPage, Bson sort, Predicate<EntityGroup> filter) {
final SearchQuery searchQuery = searchQueryParser.parse(query);
return filter == null ?
paginationHelper.filter(searchQuery.toBson()).sort(sort).perPage(perPage).page(page) :
paginationHelper.filter(searchQuery.toBson()).sort(sort).perPage(perPage).page(page, filter);
}

public EntityGroup save(EntityGroup EntityGroup) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public EntityGroup save(EntityGroup EntityGroup) {
public EntityGroup save(EntityGroup entityGroup) {

Camel case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catches! Looks like some bad find-and-replaces from the original categories verbiage.

if (EntityGroup.id() != null) {
return scopedEntityMongoUtils.update(EntityGroup);
}
String newId = scopedEntityMongoUtils.create(EntityGroup);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When trying to update an entity group (without supplying an ID in the request JSON), the update call ends up creating a new copy of the entity. I think some other recently developed API requests don't rely on the ID being in the JSON but instead use the ID from the URL to look up the existing entity (and error if it's not found). I think doing the same here would be good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah nice catch, just updated to use the url ID.

return EntityGroup.toBuilder().id(newId).build();
}

public Optional<EntityGroup> getByName(String name) {
final Bson query = eq(EntityGroup.FIELD_NAME, name);

return Optional.ofNullable(collection.find(query).first());
}

public List<EntityGroup> getAllForEntity(EntityType type, String entityId) {
final Bson query = and(
exists(typeField(type)),
in(typeField(type), entityId)
);
return MongoUtils.stream(collection.find(query)).toList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, please allow the caller to make the decision whether they want a stream or a list. toList in database services are usually not the right approach.

}

public long delete(String id) {
return collection.deleteOne(MongoUtils.idEq(id)).getDeletedCount();
}

private String typeField(EntityType type) {
return EntityGroup.FIELD_ENTITIES + "." + type.getName();
}
}
Loading
Loading