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"; public static final String MLMODEL = "mlmodel";
// Not deleted to ensure the ordinal value of the entities after this remains the same // 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 UNUSED = "unused";
public static final String BOTS = "bots"; public static final String BOT = "bot";
public static final String THREAD = "THREAD"; public static final String THREAD = "THREAD";
public static final String LOCATION = "location"; public static final String LOCATION = "location";
public static final String GLOSSARY = "glossary"; public static final String GLOSSARY = "glossary";
@ -122,7 +122,7 @@ public final class Entity {
TEAM, TEAM,
ROLE, ROLE,
POLICY, POLICY,
BOTS, BOT,
INGESTION_PIPELINE, INGESTION_PIPELINE,
DATABASE_SERVICE, DATABASE_SERVICE,
PIPELINE_SERVICE, PIPELINE_SERVICE,
@ -247,8 +247,7 @@ public final class Entity {
} }
public static void deleteEntity( public static void deleteEntity(
String updatedBy, String entityType, UUID entityId, boolean recursive, boolean hardDelete, boolean internal) String updatedBy, String entityType, UUID entityId, boolean recursive, boolean hardDelete) throws IOException {
throws IOException {
EntityRepository<?> dao = getEntityRepository(entityType); EntityRepository<?> dao = getEntityRepository(entityType);
dao.delete(updatedBy, entityId.toString(), recursive, hardDelete); dao.delete(updatedBy, entityId.toString(), recursive, hardDelete);
} }

View File

@ -14,108 +14,20 @@
package org.openmetadata.catalog.elasticsearch; package org.openmetadata.catalog.elasticsearch;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
public class ElasticSearchConfiguration { public class ElasticSearchConfiguration {
@NotEmpty @Getter @Setter private String host;
@NotEmpty private String host; @NotEmpty @Getter @Setter private Integer port;
@Getter @Setter private String username;
@NotEmpty private Integer port; @Getter @Setter private String password;
@Getter @Setter private String scheme;
private String username; @Getter @Setter private String truststorePath;
@Getter @Setter private String truststorePassword;
private String password; @Getter @Setter private Integer connectionTimeoutSecs = 5;
@Getter @Setter private Integer socketTimeoutSecs = 60;
private String scheme; @Getter @Setter private Integer batchSize = 10;
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;
}
@Override @Override
public String toString() { public String toString() {

View File

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

View File

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

View File

@ -15,9 +15,10 @@ package org.openmetadata.catalog.exception;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import lombok.Getter;
public abstract class WebServiceException extends RuntimeException { public abstract class WebServiceException extends RuntimeException {
private final transient Response response; @Getter private final transient Response response;
protected WebServiceException(Response.Status status, String msg) { protected WebServiceException(Response.Status status, String msg) {
super(msg); super(msg);
@ -41,20 +42,12 @@ public abstract class WebServiceException extends RuntimeException {
return new ErrorResponse(msg); return new ErrorResponse(msg);
} }
public Response getResponse() {
return response;
}
private static class ErrorResponse { private static class ErrorResponse {
/** Response message. */ /** Response message. */
private final String responseMessage; @Getter private final String responseMessage;
ErrorResponse(String responseMessage) { ErrorResponse(String responseMessage) {
this.responseMessage = 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.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.openmetadata.catalog.Entity; import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.entity.Bots; import org.openmetadata.catalog.entity.Bot;
import org.openmetadata.catalog.resources.bots.BotsResource; 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.ChangeDescription;
import org.openmetadata.catalog.type.EntityReference; 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.EntityInterface;
import org.openmetadata.catalog.util.EntityUtil.Fields; import org.openmetadata.catalog.util.EntityUtil.Fields;
public class BotsRepository extends EntityRepository<Bots> { public class BotRepository extends EntityRepository<Bot> {
public BotsRepository(CollectionDAO dao) { public BotRepository(CollectionDAO dao) {
super(BotsResource.COLLECTION_PATH, Entity.BOTS, Bots.class, dao.botsDAO(), dao, "", ""); super(BotResource.COLLECTION_PATH, Entity.BOT, Bot.class, dao.botDAO(), dao, "", "");
} }
@Override @Override
public Bots setFields(Bots entity, Fields fields) { public Bot setFields(Bot entity, Fields fields) throws IOException {
return entity; return entity.withBotUser(getBotUser(entity));
} }
@Override @Override
public EntityInterface<Bots> getEntityInterface(Bots entity) { public EntityInterface<Bot> getEntityInterface(Bot entity) {
return new BotsEntityInterface(entity); return new BotEntityInterface(entity);
} }
@Override @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 @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); store(entity.getId(), entity, update);
entity.withBotUser(botUser);
} }
@Override @Override
public void storeRelationships(Bots entity) { public void storeRelationships(Bot entity) {
/* Nothing to do */ addRelationship(entity.getId(), entity.getBotUser().getId(), Entity.BOT, Entity.USER, Relationship.CONTAINS);
} }
public static class BotsEntityInterface extends EntityInterface<Bots> { @Override
public BotsEntityInterface(Bots entity) { public EntityRepository<Bot>.EntityUpdater getUpdater(Bot original, Bot updated, Operation operation) {
super(Entity.BOTS, entity); 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 @Override
@ -113,7 +142,7 @@ public class BotsRepository extends EntityRepository<Bots> {
} }
@Override @Override
public Bots getEntity() { public Bot getEntity() {
return entity; return entity;
} }
@ -160,8 +189,14 @@ public class BotsRepository extends EntityRepository<Bots> {
} }
@Override @Override
public Bots withHref(URI href) { public Bot withHref(URI href) {
return entity.withHref(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.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate; import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.openmetadata.catalog.Entity; 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.Chart;
import org.openmetadata.catalog.entity.data.Dashboard; import org.openmetadata.catalog.entity.data.Dashboard;
import org.openmetadata.catalog.entity.data.Database; 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.Role;
import org.openmetadata.catalog.entity.teams.Team; import org.openmetadata.catalog.entity.teams.Team;
import org.openmetadata.catalog.entity.teams.User; 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.ChartRepository.ChartEntityInterface;
import org.openmetadata.catalog.jdbi3.CollectionDAO.TagUsageDAO.TagLabelMapper; import org.openmetadata.catalog.jdbi3.CollectionDAO.TagUsageDAO.TagLabelMapper;
import org.openmetadata.catalog.jdbi3.CollectionDAO.UsageDAO.UsageDetailsMapper; import org.openmetadata.catalog.jdbi3.CollectionDAO.UsageDAO.UsageDetailsMapper;
@ -164,7 +164,7 @@ public interface CollectionDAO {
GlossaryTermDAO glossaryTermDAO(); GlossaryTermDAO glossaryTermDAO();
@CreateSqlObject @CreateSqlObject
BotsDAO botsDAO(); BotDAO botDAO();
@CreateSqlObject @CreateSqlObject
PolicyDAO policyDAO(); PolicyDAO policyDAO();
@ -860,25 +860,25 @@ public interface CollectionDAO {
} }
} }
interface BotsDAO extends EntityDAO<Bots> { interface BotDAO extends EntityDAO<Bot> {
@Override @Override
default String getTableName() { default String getTableName() {
return "bots_entity"; return "bot_entity";
} }
@Override @Override
default Class<Bots> getEntityClass() { default Class<Bot> getEntityClass() {
return Bots.class; return Bot.class;
} }
@Override @Override
default String getNameColumn() { default String getNameColumn() {
return "fullyQualifiedName"; return "name";
} }
@Override @Override
default EntityReference getEntityReference(Bots entity) { default EntityReference getEntityReference(Bot entity) {
return new BotsEntityInterface(entity).getEntityReference(); return new BotEntityInterface(entity).getEntityReference();
} }
} }

View File

@ -529,6 +529,7 @@ public abstract class EntityRepository<T> {
cleanup(entityInterface); cleanup(entityInterface);
changeType = RestUtil.ENTITY_DELETED; changeType = RestUtil.ENTITY_DELETED;
} }
LOG.info("{} deleted {} {}", hardDelete ? "Hard" : "Soft", entityInterface.getFullyQualifiedName());
return new DeleteResponse<>(updated, changeType); return new DeleteResponse<>(updated, changeType);
} }
@ -546,8 +547,12 @@ public abstract class EntityRepository<T> {
} }
// Delete all the contained entities // Delete all the contained entities
for (EntityReference entityReference : contains) { for (EntityReference entityReference : contains) {
LOG.info("Recursively deleting {} {}", entityReference.getType(), entityReference.getId()); LOG.info(
Entity.deleteEntity(updatedBy, entityReference.getType(), entityReference.getId(), true, hardDelete, true); "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; package org.openmetadata.catalog.security;
import java.security.Principal; import java.security.Principal;
import lombok.Getter;
import lombok.Setter;
import org.openmetadata.catalog.entity.teams.User; import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.util.EntityUtil.Fields; import org.openmetadata.catalog.util.EntityUtil.Fields;
/** Holds context information of authenticated user, which will be used for authorization. */ /** Holds context information of authenticated user, which will be used for authorization. */
public final class AuthenticationContext { public final class AuthenticationContext {
private final Principal principal; @Getter private final Principal principal;
private User user; @Getter @Setter private User user;
private Fields userFields; @Getter @Setter private Fields userFields;
public AuthenticationContext(Principal principal) { public AuthenticationContext(Principal principal) {
this.principal = principal; this.principal = principal;
} }
public Principal getPrincipal() {
return principal;
}
@Override @Override
public String toString() { public String toString() {
return "AuthenticationContext{" + ", principal=" + principal + '}'; 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.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import lombok.Getter;
public class AuthenticationException extends RuntimeException { public class AuthenticationException extends RuntimeException {
private final transient Response response; @Getter private final transient Response response;
public AuthenticationException(String msg) { public AuthenticationException(String msg) {
super(msg); super(msg);
@ -41,20 +42,12 @@ public class AuthenticationException extends RuntimeException {
return new ErrorResponse(msg); return new ErrorResponse(msg);
} }
public Response getResponse() {
return response;
}
private static class ErrorResponse { private static class ErrorResponse {
/** Response message. */ /** Response message. */
private final String responseMessage; @Getter private final String responseMessage;
ErrorResponse(String responseMessage) { ErrorResponse(String responseMessage) {
this.responseMessage = 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.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import lombok.Getter;
public class AuthorizationException extends RuntimeException { public class AuthorizationException extends RuntimeException {
private final transient Response response; @Getter private final transient Response response;
public AuthorizationException(String msg) { public AuthorizationException(String msg) {
super(msg); super(msg);
@ -41,20 +42,12 @@ public class AuthorizationException extends RuntimeException {
return new ErrorResponse(msg); return new ErrorResponse(msg);
} }
public Response getResponse() {
return response;
}
private static class ErrorResponse { private static class ErrorResponse {
/** Response message. */ /** Response message. */
private final String responseMessage; @Getter private final String responseMessage;
ErrorResponse(String responseMessage) { ErrorResponse(String responseMessage) {
this.responseMessage = responseMessage; this.responseMessage = responseMessage;
} }
public String getResponseMessage() {
return responseMessage;
}
} }
} }

View File

@ -14,19 +14,15 @@
package org.openmetadata.catalog.security; package org.openmetadata.catalog.security;
import java.security.Principal; import java.security.Principal;
import lombok.Getter;
public class CatalogPrincipal implements Principal { public class CatalogPrincipal implements Principal {
private final String name; @Getter private final String name;
public CatalogPrincipal(String name) { public CatalogPrincipal(String name) {
this.name = name; this.name = name;
} }
@Override
public String getName() {
return name;
}
@Override @Override
public String toString() { public String toString() {
return "CatalogPrincipal{" + "name='" + name + '\'' + '}'; return "CatalogPrincipal{" + "name='" + name + '\'' + '}';

View File

@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
/** Holds authenticated principal and security context which is passed to the JAX-RS request methods */ /** Holds authenticated principal and security context which is passed to the JAX-RS request methods */
public class CatalogSecurityContext implements SecurityContext { public class CatalogSecurityContext implements SecurityContext {
private final Principal principal; private final Principal principal;
private final String scheme; private final String scheme;
private final String authenticationScheme; private final String authenticationScheme;

View File

@ -15,6 +15,7 @@ import java.time.ZoneId;
import java.util.Base64; import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.entity.teams.User; import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.teams.authn.JWTTokenExpiry; import org.openmetadata.catalog.teams.authn.JWTTokenExpiry;
@ -23,7 +24,7 @@ import org.openmetadata.catalog.teams.authn.JWTTokenExpiry;
public class JWTTokenGenerator { public class JWTTokenGenerator {
private static volatile JWTTokenGenerator instance; private static volatile JWTTokenGenerator instance;
private RSAPrivateKey privateKey; private RSAPrivateKey privateKey;
private RSAPublicKey publicKey; @Getter private RSAPublicKey publicKey;
private String issuer; private String issuer;
private String kid; private String kid;
@ -66,10 +67,6 @@ public class JWTTokenGenerator {
} }
} }
public RSAPublicKey getPublicKey() {
return publicKey;
}
public String generateJWTToken(User user, JWTTokenExpiry expiry) { public String generateJWTToken(User user, JWTTokenExpiry expiry) {
try { try {
Algorithm algorithm = Algorithm.RSA256(null, privateKey); 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", "$id": "https://open-metadata.org/schema/entity/bots.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "Bot", "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", "type": "object",
"properties": { "properties": {
@ -22,6 +22,10 @@
"description": "Description of the bot.", "description": "Description of the bot.",
"type": "string" "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": { "version": {
"description": "Metadata version of the entity.", "description": "Metadata version of the entity.",
"$ref": "../type/entityHistory.json#/definitions/entityVersion" "$ref": "../type/entityHistory.json#/definitions/entityVersion"
@ -48,5 +52,6 @@
"default": false "default": false
} }
}, },
"required": ["id", "name", "botUser"],
"additionalProperties": false "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.Response;
import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.resources.events.EventResource.ChangeEventList; import org.openmetadata.catalog.resources.events.EventResource.ChangeEventList;
import org.openmetadata.catalog.type.ChangeEvent; 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 */ /** Class to keep track of all the events received by a webhook endpoint */
static class EventDetails { static class EventDetails {
long firstEventTime; @Getter @Setter long firstEventTime;
long latestEventTime; @Getter @Setter long latestEventTime;
ConcurrentLinkedQueue<ChangeEvent> events = new ConcurrentLinkedQueue<>(); @Getter 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;
}
} }
} }