Fixes #4749 Add all the required APIs for Bot entity along with tests (#4750)

This commit is contained in:
Suresh Srinivas 2022-05-05 15:09:06 -07:00 committed by GitHub
parent 9138b85913
commit e6abd86797
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 561 additions and 385 deletions

View File

@ -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);
}

View File

@ -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() {

View File

@ -13,15 +13,9 @@
package org.openmetadata.catalog.events;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
public class EventHandlerConfiguration {
private Set<String> eventHandlerClassNames;
public Set<String> getEventHandlerClassNames() {
return eventHandlerClassNames;
}
public void setEventHandlerClassNames(Set<String> eventHandlerClassNames) {
this.eventHandlerClassNames = eventHandlerClassNames;
}
@Getter @Setter private Set<String> eventHandlerClassNames;
}

View File

@ -1,21 +1,10 @@
package org.openmetadata.catalog.events;
import java.util.Map;
import lombok.Getter;
public class EventPublisherConfiguration {
String name;
String className;
Map<String, Object> config;
public String getName() {
return name;
}
public String getClassName() {
return className;
}
public Map<String, Object> getConfig() {
return config;
}
@Getter String name;
@Getter String className;
@Getter Map<String, Object> config;
}

View File

@ -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;
}
}
}

View File

@ -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<Bots> {
public BotsRepository(CollectionDAO dao) {
super(BotsResource.COLLECTION_PATH, Entity.BOTS, Bots.class, dao.botsDAO(), dao, "", "");
public class BotRepository extends EntityRepository<Bot> {
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<Bots> getEntityInterface(Bots entity) {
return new BotsEntityInterface(entity);
public EntityInterface<Bot> 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<Bots> {
public BotsEntityInterface(Bots entity) {
super(Entity.BOTS, entity);
@Override
public EntityRepository<Bot>.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<String> 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<Bot> {
public BotEntityInterface(Bot entity) {
super(Entity.BOT, entity);
}
@Override
@ -113,7 +142,7 @@ public class BotsRepository extends EntityRepository<Bots> {
}
@Override
public Bots getEntity() {
public Bot getEntity() {
return entity;
}
@ -160,8 +189,14 @@ public class BotsRepository extends EntityRepository<Bots> {
}
@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);
}
}
}

View File

@ -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<Bots> {
interface BotDAO extends EntityDAO<Bot> {
@Override
default String getTableName() {
return "bots_entity";
return "bot_entity";
}
@Override
default Class<Bots> getEntityClass() {
return Bots.class;
default Class<Bot> 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();
}
}

View File

@ -529,6 +529,7 @@ public abstract class EntityRepository<T> {
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<T> {
}
// 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);
}
}

View File

@ -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<Bot, BotRepository> {
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<Bot> {
@SuppressWarnings("unused")
public BotList() {
/* Required for serde */
}
public BotList(List<Bot> 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<Bot> 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());
}
}

View File

@ -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<Bots, BotsRepository> {
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<Bots> {
public BotsList(List<Bots> 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<Bots> 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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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 + '\'' + '}';

View File

@ -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;

View File

@ -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);

View File

@ -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
}

View File

@ -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
}

View File

@ -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<Bot, CreateBot> {
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<String, String> 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<String, String> authHeaders) throws HttpResponseException {
validateCommonEntityFields(
getEntityInterface(updated), expected.getDescription(), TestUtils.getPrincipal(authHeaders), null);
assertReference(expected.getBotUser(), updated.getBotUser());
}
@Override
public EntityInterface<Bot> getEntityInterface(Bot entity) {
return new BotEntityInterface(entity);
}
@Override
public EntityInterface<Bot> 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 {}
}

View File

@ -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<ChangeEvent> 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<ChangeEvent> getEvents() {
return events;
}
@Getter @Setter long firstEventTime;
@Getter @Setter long latestEventTime;
@Getter ConcurrentLinkedQueue<ChangeEvent> events = new ConcurrentLinkedQueue<>();
}
}