diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java index 7b67e2fe6f9..77523088ef7 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java @@ -87,7 +87,7 @@ public final class Entity { public static final String MLMODEL = "mlmodel"; // Not deleted to ensure the ordinal value of the entities after this remains the same public static final String UNUSED = "unused"; - public static final String BOTS = "bots"; + public static final String BOT = "bot"; public static final String THREAD = "THREAD"; public static final String LOCATION = "location"; public static final String GLOSSARY = "glossary"; @@ -122,7 +122,7 @@ public final class Entity { TEAM, ROLE, POLICY, - BOTS, + BOT, INGESTION_PIPELINE, DATABASE_SERVICE, PIPELINE_SERVICE, @@ -247,8 +247,7 @@ public final class Entity { } public static void deleteEntity( - String updatedBy, String entityType, UUID entityId, boolean recursive, boolean hardDelete, boolean internal) - throws IOException { + String updatedBy, String entityType, UUID entityId, boolean recursive, boolean hardDelete) throws IOException { EntityRepository dao = getEntityRepository(entityType); dao.delete(updatedBy, entityId.toString(), recursive, hardDelete); } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/elasticsearch/ElasticSearchConfiguration.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/elasticsearch/ElasticSearchConfiguration.java index 2d161fce0eb..275e44de021 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/elasticsearch/ElasticSearchConfiguration.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/elasticsearch/ElasticSearchConfiguration.java @@ -14,108 +14,20 @@ package org.openmetadata.catalog.elasticsearch; import javax.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; public class ElasticSearchConfiguration { - - @NotEmpty private String host; - - @NotEmpty private Integer port; - - private String username; - - private String password; - - private String scheme; - - private String truststorePath; - - private String truststorePassword; - - private Integer connectionTimeoutSecs = 5; - - private Integer socketTimeoutSecs = 60; - - private Integer batchSize = 10; - - public String getHost() { - return host; - } - - public void setHost(String host) { - this.host = host; - } - - public Integer getPort() { - return port; - } - - public void setPort(Integer port) { - this.port = port; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getScheme() { - return scheme; - } - - public void setScheme(String scheme) { - this.scheme = scheme; - } - - public String getTruststorePath() { - return truststorePath; - } - - public void setTruststorePath(String truststorePath) { - this.truststorePath = truststorePath; - } - - public String getTruststorePassword() { - return truststorePassword; - } - - public void setTruststorePassword(String truststorePassword) { - this.truststorePassword = truststorePassword; - } - - public Integer getConnectionTimeoutSecs() { - return connectionTimeoutSecs; - } - - public void setConnectionTimeoutSecs(Integer connectionTimeoutSecs) { - this.connectionTimeoutSecs = connectionTimeoutSecs; - } - - public Integer getSocketTimeoutSecs() { - return socketTimeoutSecs; - } - - public void setSocketTimeoutSecs(Integer socketTimeoutSecs) { - this.socketTimeoutSecs = socketTimeoutSecs; - } - - public Integer getBatchSize() { - return batchSize; - } - - public void setBatchSize(Integer batchSize) { - this.batchSize = batchSize; - } + @NotEmpty @Getter @Setter private String host; + @NotEmpty @Getter @Setter private Integer port; + @Getter @Setter private String username; + @Getter @Setter private String password; + @Getter @Setter private String scheme; + @Getter @Setter private String truststorePath; + @Getter @Setter private String truststorePassword; + @Getter @Setter private Integer connectionTimeoutSecs = 5; + @Getter @Setter private Integer socketTimeoutSecs = 60; + @Getter @Setter private Integer batchSize = 10; @Override public String toString() { diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventHandlerConfiguration.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventHandlerConfiguration.java index 219a73f2421..49da8b934ba 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventHandlerConfiguration.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventHandlerConfiguration.java @@ -13,15 +13,9 @@ package org.openmetadata.catalog.events; import java.util.Set; +import lombok.Getter; +import lombok.Setter; public class EventHandlerConfiguration { - private Set eventHandlerClassNames; - - public Set getEventHandlerClassNames() { - return eventHandlerClassNames; - } - - public void setEventHandlerClassNames(Set eventHandlerClassNames) { - this.eventHandlerClassNames = eventHandlerClassNames; - } + @Getter @Setter private Set eventHandlerClassNames; } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventPublisherConfiguration.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventPublisherConfiguration.java index afddd1f1961..cf716ffb9b8 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventPublisherConfiguration.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/EventPublisherConfiguration.java @@ -1,21 +1,10 @@ package org.openmetadata.catalog.events; import java.util.Map; +import lombok.Getter; public class EventPublisherConfiguration { - String name; - String className; - Map config; - - public String getName() { - return name; - } - - public String getClassName() { - return className; - } - - public Map getConfig() { - return config; - } + @Getter String name; + @Getter String className; + @Getter Map config; } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/exception/WebServiceException.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/exception/WebServiceException.java index 9a0af33617c..63a0287e7a1 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/exception/WebServiceException.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/exception/WebServiceException.java @@ -15,9 +15,10 @@ package org.openmetadata.catalog.exception; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import lombok.Getter; public abstract class WebServiceException extends RuntimeException { - private final transient Response response; + @Getter private final transient Response response; protected WebServiceException(Response.Status status, String msg) { super(msg); @@ -41,20 +42,12 @@ public abstract class WebServiceException extends RuntimeException { return new ErrorResponse(msg); } - public Response getResponse() { - return response; - } - private static class ErrorResponse { /** Response message. */ - private final String responseMessage; + @Getter private final String responseMessage; ErrorResponse(String responseMessage) { this.responseMessage = responseMessage; } - - public String getResponseMessage() { - return responseMessage; - } } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/BotsRepository.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/BotRepository.java similarity index 56% rename from catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/BotsRepository.java rename to catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/BotRepository.java index 9bfbdf4e1a9..232b561671e 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/BotsRepository.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/BotRepository.java @@ -15,46 +15,75 @@ package org.openmetadata.catalog.jdbi3; import java.io.IOException; import java.net.URI; +import java.util.List; import java.util.UUID; import org.openmetadata.catalog.Entity; -import org.openmetadata.catalog.entity.Bots; -import org.openmetadata.catalog.resources.bots.BotsResource; +import org.openmetadata.catalog.entity.Bot; +import org.openmetadata.catalog.entity.teams.User; +import org.openmetadata.catalog.resources.bots.BotResource; import org.openmetadata.catalog.type.ChangeDescription; import org.openmetadata.catalog.type.EntityReference; +import org.openmetadata.catalog.type.Include; +import org.openmetadata.catalog.type.Relationship; import org.openmetadata.catalog.util.EntityInterface; import org.openmetadata.catalog.util.EntityUtil.Fields; -public class BotsRepository extends EntityRepository { - public BotsRepository(CollectionDAO dao) { - super(BotsResource.COLLECTION_PATH, Entity.BOTS, Bots.class, dao.botsDAO(), dao, "", ""); +public class BotRepository extends EntityRepository { + public BotRepository(CollectionDAO dao) { + super(BotResource.COLLECTION_PATH, Entity.BOT, Bot.class, dao.botDAO(), dao, "", ""); } @Override - public Bots setFields(Bots entity, Fields fields) { - return entity; + public Bot setFields(Bot entity, Fields fields) throws IOException { + return entity.withBotUser(getBotUser(entity)); } @Override - public EntityInterface getEntityInterface(Bots entity) { - return new BotsEntityInterface(entity); + public EntityInterface getEntityInterface(Bot entity) { + return new BotEntityInterface(entity); } @Override - public void prepare(Bots entity) {} + public void prepare(Bot entity) throws IOException { + User user = daoCollection.userDAO().findEntityById(entity.getBotUser().getId(), Include.ALL); + entity.getBotUser().withName(user.getName()).withDisplayName(user.getDisplayName()); + } @Override - public void storeEntity(Bots entity, boolean update) throws IOException { + public void storeEntity(Bot entity, boolean update) throws IOException { + EntityReference botUser = entity.getBotUser(); + entity.withBotUser(null); store(entity.getId(), entity, update); + entity.withBotUser(botUser); } @Override - public void storeRelationships(Bots entity) { - /* Nothing to do */ + public void storeRelationships(Bot entity) { + addRelationship(entity.getId(), entity.getBotUser().getId(), Entity.BOT, Entity.USER, Relationship.CONTAINS); } - public static class BotsEntityInterface extends EntityInterface { - public BotsEntityInterface(Bots entity) { - super(Entity.BOTS, entity); + @Override + public EntityRepository.EntityUpdater getUpdater(Bot original, Bot updated, Operation operation) { + return new BotUpdater(original, updated, operation); + } + + @Override + public void restorePatchAttributes(Bot original, Bot updated) { + // Bot user can't be changed by patch + updated.withBotUser(original.getBotUser()); + } + + public EntityReference getBotUser(Bot bot) throws IOException { + List refs = findTo(bot.getId(), Entity.BOT, Relationship.CONTAINS, Entity.USER); + ensureSingleRelationship(Entity.BOT, bot.getId(), refs, "botUser", true); + return refs.isEmpty() + ? null + : daoCollection.userDAO().findEntityReferenceById(UUID.fromString(refs.get(0)), Include.ALL); + } + + public static class BotEntityInterface extends EntityInterface { + public BotEntityInterface(Bot entity) { + super(Entity.BOT, entity); } @Override @@ -113,7 +142,7 @@ public class BotsRepository extends EntityRepository { } @Override - public Bots getEntity() { + public Bot getEntity() { return entity; } @@ -160,8 +189,14 @@ public class BotsRepository extends EntityRepository { } @Override - public Bots withHref(URI href) { + public Bot withHref(URI href) { return entity.withHref(href); } } + + public class BotUpdater extends EntityUpdater { + public BotUpdater(Bot original, Bot updated, Operation operation) { + super(original, updated, operation); + } + } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/CollectionDAO.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/CollectionDAO.java index 09562f48db7..1c602cd627d 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/CollectionDAO.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/CollectionDAO.java @@ -32,7 +32,7 @@ import org.jdbi.v3.sqlobject.customizer.Define; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; import org.openmetadata.catalog.Entity; -import org.openmetadata.catalog.entity.Bots; +import org.openmetadata.catalog.entity.Bot; import org.openmetadata.catalog.entity.data.Chart; import org.openmetadata.catalog.entity.data.Dashboard; import org.openmetadata.catalog.entity.data.Database; @@ -56,7 +56,7 @@ import org.openmetadata.catalog.entity.services.ingestionPipelines.IngestionPipe import org.openmetadata.catalog.entity.teams.Role; import org.openmetadata.catalog.entity.teams.Team; import org.openmetadata.catalog.entity.teams.User; -import org.openmetadata.catalog.jdbi3.BotsRepository.BotsEntityInterface; +import org.openmetadata.catalog.jdbi3.BotRepository.BotEntityInterface; import org.openmetadata.catalog.jdbi3.ChartRepository.ChartEntityInterface; import org.openmetadata.catalog.jdbi3.CollectionDAO.TagUsageDAO.TagLabelMapper; import org.openmetadata.catalog.jdbi3.CollectionDAO.UsageDAO.UsageDetailsMapper; @@ -164,7 +164,7 @@ public interface CollectionDAO { GlossaryTermDAO glossaryTermDAO(); @CreateSqlObject - BotsDAO botsDAO(); + BotDAO botDAO(); @CreateSqlObject PolicyDAO policyDAO(); @@ -860,25 +860,25 @@ public interface CollectionDAO { } } - interface BotsDAO extends EntityDAO { + interface BotDAO extends EntityDAO { @Override default String getTableName() { - return "bots_entity"; + return "bot_entity"; } @Override - default Class getEntityClass() { - return Bots.class; + default Class getEntityClass() { + return Bot.class; } @Override default String getNameColumn() { - return "fullyQualifiedName"; + return "name"; } @Override - default EntityReference getEntityReference(Bots entity) { - return new BotsEntityInterface(entity).getEntityReference(); + default EntityReference getEntityReference(Bot entity) { + return new BotEntityInterface(entity).getEntityReference(); } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/EntityRepository.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/EntityRepository.java index 42abfcfb822..5df0a3e0c36 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/EntityRepository.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/EntityRepository.java @@ -529,6 +529,7 @@ public abstract class EntityRepository { cleanup(entityInterface); changeType = RestUtil.ENTITY_DELETED; } + LOG.info("{} deleted {} {}", hardDelete ? "Hard" : "Soft", entityInterface.getFullyQualifiedName()); return new DeleteResponse<>(updated, changeType); } @@ -546,8 +547,12 @@ public abstract class EntityRepository { } // Delete all the contained entities for (EntityReference entityReference : contains) { - LOG.info("Recursively deleting {} {}", entityReference.getType(), entityReference.getId()); - Entity.deleteEntity(updatedBy, entityReference.getType(), entityReference.getId(), true, hardDelete, true); + LOG.info( + "Recursively {} deleting {} {}", + hardDelete ? "hard" : "soft", + entityReference.getType(), + entityReference.getId()); + Entity.deleteEntity(updatedBy, entityReference.getType(), entityReference.getId(), true, hardDelete); } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/bots/BotResource.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/bots/BotResource.java new file mode 100644 index 00000000000..c6f3be4b104 --- /dev/null +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/bots/BotResource.java @@ -0,0 +1,314 @@ +/* + * Copyright 2021 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmetadata.catalog.resources.bots; + +import static org.openmetadata.catalog.security.SecurityUtil.ADMIN; + +import io.swagger.annotations.Api; +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import javax.json.JsonPatch; +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.PATCH; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import org.openmetadata.catalog.Entity; +import org.openmetadata.catalog.api.CreateBot; +import org.openmetadata.catalog.entity.Bot; +import org.openmetadata.catalog.jdbi3.BotRepository; +import org.openmetadata.catalog.jdbi3.CollectionDAO; +import org.openmetadata.catalog.jdbi3.ListFilter; +import org.openmetadata.catalog.resources.Collection; +import org.openmetadata.catalog.resources.EntityResource; +import org.openmetadata.catalog.security.Authorizer; +import org.openmetadata.catalog.type.EntityHistory; +import org.openmetadata.catalog.type.Include; +import org.openmetadata.catalog.util.ResultList; + +@Path("/v1/bots") +@Api(value = "Bot collection", tags = "Bot collection") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Collection(name = "bots") +public class BotResource extends EntityResource { + public static final String COLLECTION_PATH = "/v1/bots/"; + + public BotResource(CollectionDAO dao, Authorizer authorizer) { + super(Bot.class, new BotRepository(dao), authorizer); + } + + @Override + public Bot addHref(UriInfo uriInfo, Bot entity) { + Entity.withHref(uriInfo, entity.getBotUser()); + return entity; + } + + public static class BotList extends ResultList { + @SuppressWarnings("unused") + public BotList() { + /* Required for serde */ + } + + public BotList(List data) { + super(data); + } + } + + @GET + @Operation( + summary = "List Bot", + tags = "bots", + description = "Get a list of Bot.", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of Bot", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = BotList.class))) + }) + public ResultList list( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @DefaultValue("10") @Min(0) @Max(1000000) @QueryParam("limit") int limitParam, + @Parameter(description = "Returns list of Bot before this cursor", schema = @Schema(type = "string")) + @QueryParam("before") + String before, + @Parameter(description = "Returns list of Bot after this cursor", schema = @Schema(type = "string")) + @QueryParam("after") + String after, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) + throws IOException { + return listInternal(uriInfo, securityContext, "", new ListFilter(include), limitParam, before, after); + } + + @GET + @Path("/{id}") + @Operation( + summary = "Get a bot", + tags = "bots", + description = "Get a bot by `id`.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The bot", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Bot.class))), + @ApiResponse(responseCode = "404", description = "Bot for instance {id} is not found") + }) + public Bot get( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @QueryParam("include") @DefaultValue("non-deleted") Include include, + @PathParam("id") String id) + throws IOException { + return getInternal(uriInfo, securityContext, id, "", include); + } + + @GET + @Path("/name/{fqn}") + @Operation( + summary = "Get a bot by name", + tags = "bots", + description = "Get a bot by name.", + responses = { + @ApiResponse( + responseCode = "200", + description = "bot", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Bot.class))), + @ApiResponse(responseCode = "404", description = "Bot for instance {name} is not found") + }) + public Bot getByName( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Fully qualified name of the table", schema = @Schema(type = "string")) @PathParam("fqn") + String fqn, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) + throws IOException { + return getByNameInternal(uriInfo, securityContext, fqn, "", include); + } + + @GET + @Path("/{id}/versions") + @Operation( + summary = "List bot versions", + tags = "bots", + description = "Get a list of all the versions of a bot identified by `id`", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of bot versions", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = EntityHistory.class))) + }) + public EntityHistory listVersions( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "bot Id", schema = @Schema(type = "string")) @PathParam("id") String id) + throws IOException { + return dao.listVersions(id); + } + + @GET + @Path("/{id}/versions/{version}") + @Operation( + summary = "Get a version of the bot", + tags = "bots", + description = "Get a version of the bot by given `id`", + responses = { + @ApiResponse( + responseCode = "200", + description = "bot", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Bot.class))), + @ApiResponse( + responseCode = "404", + description = "Bot for instance {id} and version {version} is " + "not found") + }) + public Bot getVersion( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "bot Id", schema = @Schema(type = "string")) @PathParam("id") String id, + @Parameter( + description = "bot version number in the form `major`.`minor`", + schema = @Schema(type = "string", example = "0.1 or 1.1")) + @PathParam("version") + String version) + throws IOException { + return dao.getVersion(id, version); + } + + @POST + @Operation( + summary = "Create a bot", + tags = "bots", + description = "Create a new bot.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The bot ", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Bot.class))), + @ApiResponse(responseCode = "400", description = "Bad request") + }) + public Response create(@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateBot create) + throws IOException { + Bot bot = getBot(securityContext, create); + return create(uriInfo, securityContext, bot, ADMIN); + } + + @PUT + @Operation( + summary = "Create or update a bot", + tags = "bots", + description = "Create a bot, if it does not exist. If a bot already exists, update the bot.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The bot", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = CreateBot.class))), + @ApiResponse(responseCode = "400", description = "Bad request") + }) + public Response createOrUpdate( + @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateBot create) throws IOException { + Bot bot = getBot(securityContext, create); + return createOrUpdate(uriInfo, securityContext, bot, ADMIN); + } + + @PATCH + @Path("/{id}") + @Operation( + summary = "Update a bot", + tags = "bots", + description = "Update an existing bot using JsonPatch.", + externalDocs = @ExternalDocumentation(description = "JsonPatch RFC", url = "https://tools.ietf.org/html/rfc6902")) + @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON) + public Response patch( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Id of the bot", schema = @Schema(type = "string")) @PathParam("id") String id, + @RequestBody( + description = "JsonPatch with array of operations", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON_PATCH_JSON, + examples = { + @ExampleObject("[" + "{op:remove, path:/a}," + "{op:add, path: /b, value: val}" + "]") + })) + JsonPatch patch) + throws IOException { + return patchInternal(uriInfo, securityContext, id, patch); + } + + @DELETE + @Path("/{id}") + @Operation( + summary = "Delete a bot", + tags = "bots", + description = "Delete a bot by `id`. Bot is not immediately deleted and is only marked as deleted.", + responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "Bot for instance {id} is not found") + }) + public Response delete( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Hard delete the entity. (Default = `false`)") + @QueryParam("hardDelete") + @DefaultValue("false") + boolean hardDelete, + @Parameter(description = "Id of the Bot", schema = @Schema(type = "string")) @PathParam("id") String id) + throws IOException { + return delete(uriInfo, securityContext, id, true, hardDelete, ADMIN); + } + + private Bot getBot(SecurityContext securityContext, CreateBot create) { + return new Bot() + .withId(UUID.randomUUID()) + .withName(create.getName()) + .withDescription(create.getDescription()) + .withDisplayName(create.getDisplayName()) + .withBotUser(create.getBotUser()) + .withUpdatedBy(securityContext.getUserPrincipal().getName()) + .withUpdatedAt(System.currentTimeMillis()); + } +} diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/bots/BotsResource.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/bots/BotsResource.java deleted file mode 100644 index 19aebd31d5e..00000000000 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/bots/BotsResource.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2021 Collate - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.openmetadata.catalog.resources.bots; - -import static org.openmetadata.catalog.security.SecurityUtil.ADMIN; - -import io.swagger.annotations.Api; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import java.io.IOException; -import java.util.List; -import java.util.UUID; -import javax.validation.Valid; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.ws.rs.Consumes; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.core.UriInfo; -import org.openmetadata.catalog.entity.Bots; -import org.openmetadata.catalog.jdbi3.BotsRepository; -import org.openmetadata.catalog.jdbi3.CollectionDAO; -import org.openmetadata.catalog.jdbi3.ListFilter; -import org.openmetadata.catalog.resources.Collection; -import org.openmetadata.catalog.resources.EntityResource; -import org.openmetadata.catalog.security.Authorizer; -import org.openmetadata.catalog.util.EntityUtil.Fields; -import org.openmetadata.catalog.util.ResultList; - -@Path("/v1/bots") -@Api(value = "Bots collection", tags = "Bots collection") -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Collection(name = "bots") -public class BotsResource extends EntityResource { - public static final String COLLECTION_PATH = "/v1/bots/"; - - public BotsResource(CollectionDAO dao, Authorizer authorizer) { - super(Bots.class, new BotsRepository(dao), authorizer); - } - - @Override - public Bots addHref(UriInfo uriInfo, Bots entity) { - return entity; - } - - public static class BotsList extends ResultList { - public BotsList(List data) { - super(data); - } - } - - @GET - @Operation( - summary = "List bots", - tags = "bots", - description = "Get a list of bots.", - responses = { - @ApiResponse( - responseCode = "200", - description = "List of bots", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = BotsList.class))) - }) - public ResultList list( - @Context UriInfo uriInfo, - @Context SecurityContext securityContext, - @DefaultValue("10") @Min(0) @Max(1000000) @QueryParam("limit") int limitParam, - @Parameter(description = "Returns list of bots before this cursor", schema = @Schema(type = "string")) - @QueryParam("before") - String before, - @Parameter(description = "Returns list of bots after this cursor", schema = @Schema(type = "string")) - @QueryParam("after") - String after) - throws IOException { - return super.listInternal(uriInfo, securityContext, "", new ListFilter(), limitParam, before, after); - } - - @GET - @Path("/{id}") - @Operation( - summary = "Get a bot", - tags = "bots", - description = "Get a bot by `id`.", - responses = { - @ApiResponse( - responseCode = "200", - description = "The bot", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Bots.class))), - @ApiResponse(responseCode = "404", description = "Bot for instance {id} is not found") - }) - public Bots get(@Context UriInfo uriInfo, @Context SecurityContext securityContext, @PathParam("id") String id) - throws IOException { - return dao.get(uriInfo, id, Fields.EMPTY_FIELDS); - } - - @POST - @Operation( - summary = "Create a bot", - tags = "bots", - description = "Create a new bot.", - responses = { - @ApiResponse( - responseCode = "200", - description = "The bot ", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Bots.class))), - @ApiResponse(responseCode = "400", description = "Bad request") - }) - public Response create(@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid Bots bot) - throws IOException { - bot.withId(UUID.randomUUID()) - .withUpdatedBy(securityContext.getUserPrincipal().getName()) - .withUpdatedAt(System.currentTimeMillis()); - return create(uriInfo, securityContext, bot, ADMIN); - } -} diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationContext.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationContext.java index a4339c485dd..7f462f356cc 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationContext.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationContext.java @@ -14,41 +14,23 @@ package org.openmetadata.catalog.security; import java.security.Principal; +import lombok.Getter; +import lombok.Setter; import org.openmetadata.catalog.entity.teams.User; import org.openmetadata.catalog.util.EntityUtil.Fields; /** Holds context information of authenticated user, which will be used for authorization. */ public final class AuthenticationContext { - private final Principal principal; - private User user; - private Fields userFields; + @Getter private final Principal principal; + @Getter @Setter private User user; + @Getter @Setter private Fields userFields; public AuthenticationContext(Principal principal) { this.principal = principal; } - public Principal getPrincipal() { - return principal; - } - @Override public String toString() { return "AuthenticationContext{" + ", principal=" + principal + '}'; } - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } - - public Fields getUserFields() { - return userFields; - } - - public void setUserFields(Fields userFields) { - this.userFields = userFields; - } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationException.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationException.java index 62efbd85c8f..d8f72a34a37 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationException.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthenticationException.java @@ -15,9 +15,10 @@ package org.openmetadata.catalog.security; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import lombok.Getter; public class AuthenticationException extends RuntimeException { - private final transient Response response; + @Getter private final transient Response response; public AuthenticationException(String msg) { super(msg); @@ -41,20 +42,12 @@ public class AuthenticationException extends RuntimeException { return new ErrorResponse(msg); } - public Response getResponse() { - return response; - } - private static class ErrorResponse { /** Response message. */ - private final String responseMessage; + @Getter private final String responseMessage; ErrorResponse(String responseMessage) { this.responseMessage = responseMessage; } - - public String getResponseMessage() { - return responseMessage; - } } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthorizationException.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthorizationException.java index b4e92f0cd5a..44e48bcd6c5 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthorizationException.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/AuthorizationException.java @@ -15,9 +15,10 @@ package org.openmetadata.catalog.security; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import lombok.Getter; public class AuthorizationException extends RuntimeException { - private final transient Response response; + @Getter private final transient Response response; public AuthorizationException(String msg) { super(msg); @@ -41,20 +42,12 @@ public class AuthorizationException extends RuntimeException { return new ErrorResponse(msg); } - public Response getResponse() { - return response; - } - private static class ErrorResponse { /** Response message. */ - private final String responseMessage; + @Getter private final String responseMessage; ErrorResponse(String responseMessage) { this.responseMessage = responseMessage; } - - public String getResponseMessage() { - return responseMessage; - } } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/CatalogPrincipal.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/CatalogPrincipal.java index d606ddaf906..3e6cd211c87 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/CatalogPrincipal.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/CatalogPrincipal.java @@ -14,19 +14,15 @@ package org.openmetadata.catalog.security; import java.security.Principal; +import lombok.Getter; public class CatalogPrincipal implements Principal { - private final String name; + @Getter private final String name; public CatalogPrincipal(String name) { this.name = name; } - @Override - public String getName() { - return name; - } - @Override public String toString() { return "CatalogPrincipal{" + "name='" + name + '\'' + '}'; diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/auth/CatalogSecurityContext.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/auth/CatalogSecurityContext.java index f591b3a6065..bb749b50ff3 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/auth/CatalogSecurityContext.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/auth/CatalogSecurityContext.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; @Slf4j /** Holds authenticated principal and security context which is passed to the JAX-RS request methods */ public class CatalogSecurityContext implements SecurityContext { - private final Principal principal; private final String scheme; private final String authenticationScheme; diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/jwt/JWTTokenGenerator.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/jwt/JWTTokenGenerator.java index e5b54ac2761..ad9115189d6 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/jwt/JWTTokenGenerator.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/jwt/JWTTokenGenerator.java @@ -15,6 +15,7 @@ import java.time.ZoneId; import java.util.Base64; import java.util.Date; import java.util.List; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.openmetadata.catalog.entity.teams.User; import org.openmetadata.catalog.teams.authn.JWTTokenExpiry; @@ -23,7 +24,7 @@ import org.openmetadata.catalog.teams.authn.JWTTokenExpiry; public class JWTTokenGenerator { private static volatile JWTTokenGenerator instance; private RSAPrivateKey privateKey; - private RSAPublicKey publicKey; + @Getter private RSAPublicKey publicKey; private String issuer; private String kid; @@ -66,10 +67,6 @@ public class JWTTokenGenerator { } } - public RSAPublicKey getPublicKey() { - return publicKey; - } - public String generateJWTToken(User user, JWTTokenExpiry expiry) { try { Algorithm algorithm = Algorithm.RSA256(null, privateKey); diff --git a/catalog-rest-service/src/main/resources/json/schema/api/createBot.json b/catalog-rest-service/src/main/resources/json/schema/api/createBot.json new file mode 100644 index 00000000000..da2e4d81de1 --- /dev/null +++ b/catalog-rest-service/src/main/resources/json/schema/api/createBot.json @@ -0,0 +1,28 @@ +{ + "$id": "https://open-metadata.org/schema/entity/createBot.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "createBot", + "description": "Create bot API request", + "type": "object", + + "properties": { + "name": { + "description": "Name of the bot.", + "$ref": "../type/basic.json#/definitions/entityName" + }, + "displayName": { + "description": "Name used for display purposes. Example 'FirstName LastName'.", + "type": "string" + }, + "botUser" : { + "description": "Bot user created for this bot on behalf of which the bot performs all the operations, such as updating description, responding on the conversation threads, etc.", + "$ref" : "../type/entityReference.json" + }, + "description": { + "description": "Description of the bot.", + "type": "string" + } + }, + "required": ["name", "botUser"], + "additionalProperties": false +} diff --git a/catalog-rest-service/src/main/resources/json/schema/entity/bots.json b/catalog-rest-service/src/main/resources/json/schema/entity/bot.json similarity index 76% rename from catalog-rest-service/src/main/resources/json/schema/entity/bots.json rename to catalog-rest-service/src/main/resources/json/schema/entity/bot.json index 881e08526f8..8dde7998271 100644 --- a/catalog-rest-service/src/main/resources/json/schema/entity/bots.json +++ b/catalog-rest-service/src/main/resources/json/schema/entity/bot.json @@ -2,7 +2,7 @@ "$id": "https://open-metadata.org/schema/entity/bots.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Bot", - "description": "This schema defines Bot entity. A bot automates tasks, such as adding description, identifying the importance of data. It runs as a special user in the system.", + "description": "This schema defines a Bot entity. A bot automates tasks, such as adding description, identifying the importance of data. It performs this task as a special user in the system.", "type": "object", "properties": { @@ -22,6 +22,10 @@ "description": "Description of the bot.", "type": "string" }, + "botUser" : { + "description": "Bot user created for this bot on behalf of which the bot performs all the operations, such as updating description, responding on the conversation threads, etc.", + "$ref" : "../type/entityReference.json" + }, "version": { "description": "Metadata version of the entity.", "$ref": "../type/entityHistory.json#/definitions/entityVersion" @@ -48,5 +52,6 @@ "default": false } }, + "required": ["id", "name", "botUser"], "additionalProperties": false } diff --git a/catalog-rest-service/src/test/java/org/openmetadata/catalog/resources/bots/BotResourceTest.java b/catalog-rest-service/src/test/java/org/openmetadata/catalog/resources/bots/BotResourceTest.java new file mode 100644 index 00000000000..0b0e41f801d --- /dev/null +++ b/catalog-rest-service/src/test/java/org/openmetadata/catalog/resources/bots/BotResourceTest.java @@ -0,0 +1,98 @@ +package org.openmetadata.catalog.resources.bots; + +import static org.openmetadata.catalog.util.TestUtils.ADMIN_AUTH_HEADERS; +import static org.openmetadata.catalog.util.TestUtils.getPrincipal; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; +import lombok.SneakyThrows; +import org.apache.http.client.HttpResponseException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.openmetadata.catalog.Entity; +import org.openmetadata.catalog.api.CreateBot; +import org.openmetadata.catalog.api.teams.CreateUser; +import org.openmetadata.catalog.entity.Bot; +import org.openmetadata.catalog.entity.teams.User; +import org.openmetadata.catalog.jdbi3.BotRepository.BotEntityInterface; +import org.openmetadata.catalog.jdbi3.UserRepository.UserEntityInterface; +import org.openmetadata.catalog.resources.EntityResourceTest; +import org.openmetadata.catalog.resources.bots.BotResource.BotList; +import org.openmetadata.catalog.resources.teams.UserResourceTest; +import org.openmetadata.catalog.type.EntityReference; +import org.openmetadata.catalog.util.EntityInterface; +import org.openmetadata.catalog.util.TestUtils; + +public class BotResourceTest extends EntityResourceTest { + public static User botUser; + public static EntityReference botUserRef; + + public BotResourceTest() { + super(Entity.BOT, Bot.class, BotList.class, "bots", ""); // TODO fix this + supportsFieldsQueryParam = false; + } + + @BeforeAll + public void setup(TestInfo test) throws URISyntaxException, IOException { + super.setup(test); + UserResourceTest userResourceTest = new UserResourceTest(); + CreateUser createUser = userResourceTest.createRequest("botUser", "", "", null); + botUser = new UserResourceTest().createEntity(createUser, ADMIN_AUTH_HEADERS); + botUserRef = new UserEntityInterface(botUser).getEntityReference(); + } + + @Test + void delete_ensureBotUserDelete(TestInfo test) throws IOException { + UserResourceTest userResourceTest = new UserResourceTest(); + CreateUser createUser = userResourceTest.createRequest(test); + User testUser = new UserResourceTest().createEntity(createUser, ADMIN_AUTH_HEADERS); + EntityReference testUserRef = new UserEntityInterface(testUser).getEntityReference(); + + CreateBot create = createRequest(test).withBotUser(testUserRef); + Bot bot = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); + + deleteAndCheckEntity(bot, true, true, ADMIN_AUTH_HEADERS); + + // When bot is deleted, the corresponding bot user is also deleted + assertEntityDeleted(testUser.getId(), true); + } + + @Override + public CreateBot createRequest(String name, String description, String displayName, EntityReference owner) { + return new CreateBot() + .withName(name) + .withDescription(description) + .withDisplayName(displayName) + .withBotUser(botUserRef); + } + + @SneakyThrows // TODO remove + @Override + public void validateCreatedEntity(Bot entity, CreateBot request, Map authHeaders) + throws HttpResponseException { + validateCommonEntityFields(getEntityInterface(entity), request.getDescription(), getPrincipal(authHeaders), null); + assertReference(request.getBotUser(), entity.getBotUser()); + } + + @Override + public void compareEntities(Bot expected, Bot updated, Map authHeaders) throws HttpResponseException { + validateCommonEntityFields( + getEntityInterface(updated), expected.getDescription(), TestUtils.getPrincipal(authHeaders), null); + assertReference(expected.getBotUser(), updated.getBotUser()); + } + + @Override + public EntityInterface getEntityInterface(Bot entity) { + return new BotEntityInterface(entity); + } + + @Override + public EntityInterface validateGetWithDifferentFields(Bot entity, boolean byName) throws HttpResponseException { + return new BotEntityInterface(entity); // TODO cleanup + } + + @Override + public void assertFieldChange(String fieldName, Object expected, Object actual) throws IOException {} +} diff --git a/catalog-rest-service/src/test/java/org/openmetadata/catalog/resources/events/WebhookCallbackResource.java b/catalog-rest-service/src/test/java/org/openmetadata/catalog/resources/events/WebhookCallbackResource.java index ae2fee03e9d..a7029e1a74d 100644 --- a/catalog-rest-service/src/test/java/org/openmetadata/catalog/resources/events/WebhookCallbackResource.java +++ b/catalog-rest-service/src/test/java/org/openmetadata/catalog/resources/events/WebhookCallbackResource.java @@ -19,6 +19,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriInfo; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.openmetadata.catalog.resources.events.EventResource.ChangeEventList; import org.openmetadata.catalog.type.ChangeEvent; @@ -153,28 +155,8 @@ public class WebhookCallbackResource { /** Class to keep track of all the events received by a webhook endpoint */ static class EventDetails { - long firstEventTime; - long latestEventTime; - ConcurrentLinkedQueue events = new ConcurrentLinkedQueue<>(); - - public long getFirstEventTime() { - return firstEventTime; - } - - public void setFirstEventTime(long firstEventTime) { - this.firstEventTime = firstEventTime; - } - - public long getLatestEventTime() { - return latestEventTime; - } - - public void setLatestEventTime(long latestEventTime) { - this.latestEventTime = latestEventTime; - } - - public ConcurrentLinkedQueue getEvents() { - return events; - } + @Getter @Setter long firstEventTime; + @Getter @Setter long latestEventTime; + @Getter ConcurrentLinkedQueue events = new ConcurrentLinkedQueue<>(); } }