Skip to content

Commit

Permalink
Validation of devfiles on workspace update
Browse files Browse the repository at this point in the history
  • Loading branch information
mshaposhnik authored Oct 9, 2019
1 parent cf76ba8 commit a969f69
Show file tree
Hide file tree
Showing 12 changed files with 623 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.eclipse.che.api.user.server.jpa.JpaUserDao;
import org.eclipse.che.api.user.server.spi.PreferenceDao;
import org.eclipse.che.api.user.server.spi.UserDao;
import org.eclipse.che.api.workspace.server.WorkspaceEntityProvider;
import org.eclipse.che.api.workspace.server.WorkspaceLockService;
import org.eclipse.che.api.workspace.server.WorkspaceStatusCache;
import org.eclipse.che.api.workspace.server.devfile.DevfileModule;
Expand Down Expand Up @@ -148,6 +149,7 @@ protected void configure() {

install(new DevfileModule());

bind(WorkspaceEntityProvider.class);
bind(org.eclipse.che.api.workspace.server.TemporaryWorkspaceRemover.class);
bind(org.eclipse.che.api.workspace.server.WorkspaceService.class);
install(new FactoryModuleBuilder().build(ServersCheckerFactory.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ protected void configure() {
bind(RuntimeExceptionMapper.class);
bind(ApiInfo.class).toProvider(ApiInfoProvider.class);
Multibinder.newSetBinder(binder(), Class.class, Names.named("che.json.ignored_classes"));
bind(WebApplicationExceptionMapper.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.api.core.rest;

import static org.eclipse.che.dto.server.DtoFactory.newDto;

import javax.inject.Singleton;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.NotAllowedException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.NotSupportedException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
import org.eclipse.che.dto.server.DtoFactory;

/**
* Mapper for the {@link WebApplicationException} exceptions.
*
* @author Max Shaposhnyk
*/
@Provider
@Singleton
public class WebApplicationExceptionMapper implements ExceptionMapper<WebApplicationException> {

@Override
public Response toResponse(WebApplicationException exception) {

ServiceError error = newDto(ServiceError.class).withMessage(exception.getMessage());

if (exception instanceof BadRequestException) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
} else if (exception instanceof ForbiddenException) {
return Response.status(Response.Status.FORBIDDEN)
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
} else if (exception instanceof NotFoundException) {
return Response.status(Response.Status.NOT_FOUND)
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
} else if (exception instanceof NotAuthorizedException) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
} else if (exception instanceof NotAcceptableException) {
return Response.status(Status.NOT_ACCEPTABLE)
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
} else if (exception instanceof NotAllowedException) {
return Response.status(Status.METHOD_NOT_ALLOWED)
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
} else if (exception instanceof NotSupportedException) {
return Response.status(Status.UNSUPPORTED_MEDIA_TYPE)
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
} else {
return Response.serverError()
.entity(DtoFactory.getInstance().toJson(error))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public void shouldCheckAccountPermissionsAccessOnWorkspaceCreationFromDevfile()
.post(SECURE_PATH + "/workspace/devfile?namespace=userok");

assertEquals(response.getStatusCode(), 204);
verify(workspaceService).create(anyString(), any(), any(), eq("userok"), any());
verify(workspaceService).create(any(DevfileDto.class), any(), any(), eq("userok"), any());
verify(permissionsFilter).checkAccountPermissions("userok", AccountOperation.CREATE_WORKSPACE);
verifyZeroInteractions(subject);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.api.workspace.server;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import org.eclipse.che.api.workspace.server.devfile.DevfileManager;
import org.eclipse.che.api.workspace.server.devfile.exception.DevfileFormatException;
import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto;
import org.eclipse.che.api.workspace.shared.dto.devfile.DevfileDto;
import org.eclipse.che.dto.server.DtoFactory;

/**
* Entity provider for {@link WorkspaceDto}. Performs schema validation of devfile part of the
* workspace before actual {@link DevfileDto} creation.
*
* @author Max Shaposhnyk
*/
@Singleton
@Provider
@Produces({APPLICATION_JSON})
@Consumes({APPLICATION_JSON})
public class WorkspaceEntityProvider
implements MessageBodyReader<WorkspaceDto>, MessageBodyWriter<WorkspaceDto> {

private DevfileManager devfileManager;
private ObjectMapper mapper = new ObjectMapper();

@Inject
public WorkspaceEntityProvider(DevfileManager devfileManager) {
this.devfileManager = devfileManager;
}

@Override
public boolean isReadable(
Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type == WorkspaceDto.class;
}

@Override
public WorkspaceDto readFrom(
Class<WorkspaceDto> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
InputStream entityStream)
throws IOException, WebApplicationException {
try {
JsonNode wsNode = mapper.readTree(entityStream);
JsonNode devfileNode = wsNode.path("devfile");
if (!devfileNode.isNull()) {
devfileManager.parseJson(devfileNode.toString());
}
return DtoFactory.getInstance().createDtoFromJson(wsNode.toString(), WorkspaceDto.class);
} catch (DevfileFormatException e) {
throw new BadRequestException(e.getMessage());
} catch (IOException e) {
throw new WebApplicationException(e.getMessage(), e);
}
}

@Override
public boolean isWriteable(
Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return WorkspaceDto.class.isAssignableFrom(type);
}

@Override
public long getSize(
WorkspaceDto workspaceDto,
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType) {
return -1;
}

@Override
public void writeTo(
WorkspaceDto workspaceDto,
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream)
throws IOException, WebApplicationException {
httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, "public, no-cache, no-store, no-transform");
try (Writer w = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) {
w.write(DtoFactory.getInstance().toJson(workspaceDto));
w.flush();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.eclipse.che.api.workspace.server.DtoConverter.asDto;
import static org.eclipse.che.api.workspace.server.WorkspaceKeyValidator.validateKey;
import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_AUTO_START;
import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_DEVFILE_REGISTRY_URL_PROPERTY;
import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_PLUGIN_REGISTRY_URL_PROPERTY;

import com.google.common.annotations.Beta;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
Expand Down Expand Up @@ -66,16 +64,13 @@
import org.eclipse.che.api.core.model.workspace.Workspace;
import org.eclipse.che.api.core.model.workspace.config.ServerConfig;
import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.api.workspace.server.devfile.DevfileManager;
import org.eclipse.che.api.workspace.server.devfile.FileContentProvider;
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
import org.eclipse.che.api.workspace.server.devfile.URLFileContentProvider;
import org.eclipse.che.api.workspace.server.devfile.exception.DevfileException;
import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl;
import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
import org.eclipse.che.api.workspace.server.model.impl.devfile.DevfileImpl;
import org.eclipse.che.api.workspace.server.token.MachineAccessForbidden;
import org.eclipse.che.api.workspace.server.token.MachineTokenException;
import org.eclipse.che.api.workspace.server.token.MachineTokenProvider;
Expand All @@ -89,6 +84,7 @@
import org.eclipse.che.api.workspace.shared.dto.ServerDto;
import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto;
import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto;
import org.eclipse.che.api.workspace.shared.dto.devfile.DevfileDto;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.env.EnvironmentContext;

Expand All @@ -110,7 +106,6 @@ public class WorkspaceService extends Service {
private final String apiEndpoint;
private final boolean cheWorkspaceAutoStart;
private final FileContentProvider devfileContentProvider;
private final DevfileManager devfileManager;

@Inject
public WorkspaceService(
Expand All @@ -121,8 +116,7 @@ public WorkspaceService(
WorkspaceLinksGenerator linksGenerator,
@Named(CHE_WORKSPACE_PLUGIN_REGISTRY_URL_PROPERTY) @Nullable String pluginRegistryUrl,
@Named(CHE_WORKSPACE_DEVFILE_REGISTRY_URL_PROPERTY) @Nullable String devfileRegistryUrl,
URLFetcher urlFetcher,
DevfileManager devfileManager) {
URLFetcher urlFetcher) {
this.apiEndpoint = apiEndpoint;
this.cheWorkspaceAutoStart = cheWorkspaceAutoStart;
this.workspaceManager = workspaceManager;
Expand All @@ -131,7 +125,6 @@ public WorkspaceService(
this.pluginRegistryUrl = pluginRegistryUrl;
this.devfileRegistryUrl = devfileRegistryUrl;
this.devfileContentProvider = new URLFileContentProvider(null, urlFetcher);
this.devfileManager = devfileManager;
}

@POST
Expand Down Expand Up @@ -196,16 +189,12 @@ public Response create(
return Response.status(201).entity(asDtoWithLinksAndToken(workspace)).build();
}

@Beta
@Path("/devfile")
@POST
@Consumes({APPLICATION_JSON, "text/yaml", "text/x-yaml"})
@Produces(APPLICATION_JSON)
@ApiOperation(
value = "Creates a new workspace based on the Devfile.",
notes =
"This method is in beta phase. It's strongly recommended to use `POST /devfile` instead"
+ " to get a workspace from Devfile. Workspaces created with this method are not stable yet.",
consumes = "application/json, text/yaml, text/x-yaml",
produces = APPLICATION_JSON,
nickname = "createFromDevfile",
Expand All @@ -222,7 +211,8 @@ public Response create(
@ApiResponse(code = 500, message = "Internal server error occurred")
})
public Response create(
@ApiParam(value = "The devfile of the workspace to create", required = true) String devfile,
@ApiParam(value = "The devfile of the workspace to create", required = true)
DevfileDto devfile,
@ApiParam(
value =
"Workspace attribute defined in 'attrName:attrValue' format. "
Expand All @@ -242,29 +232,15 @@ public Response create(
throws ConflictException, BadRequestException, ForbiddenException, NotFoundException,
ServerException {
requiredNotNull(devfile, "Devfile");

DevfileImpl devfileModel;
try {
if (APPLICATION_JSON_TYPE.isCompatible(contentType)) {
devfileModel = devfileManager.parseJson(devfile);
} else {
devfileModel = devfileManager.parseYaml(devfile);
}
} catch (DevfileException e) {
throw new BadRequestException(e.getMessage());
}

final Map<String, String> attributes = parseAttrs(attrsList);

if (namespace == null) {
namespace = EnvironmentContext.getCurrent().getSubject().getUserName();
}

WorkspaceImpl workspace;
try {
workspace =
workspaceManager.createWorkspace(
devfileModel,
devfile,
namespace,
attributes,
// create a new cache for each request so that we don't have to care about lifetime
Expand Down
Loading

0 comments on commit a969f69

Please sign in to comment.