/******************************************************************************* * Copyright (c) 2012-2015 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.workspace.server; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiParam; import com.wordnik.swagger.annotations.ApiResponse; import com.wordnik.swagger.annotations.ApiResponses; import org.eclipse.che.api.account.server.dao.Account; import org.eclipse.che.api.account.server.dao.AccountDao; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.ForbiddenException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.rest.Service; import org.eclipse.che.api.core.rest.annotations.Description; import org.eclipse.che.api.core.rest.annotations.GenerateLink; import org.eclipse.che.api.core.rest.annotations.Required; import org.eclipse.che.api.core.rest.shared.dto.Link; import org.eclipse.che.api.core.util.LinksHelper; import org.eclipse.che.api.project.server.ProjectService; import org.eclipse.che.api.user.server.UserService; import org.eclipse.che.api.user.server.dao.PreferenceDao; import org.eclipse.che.api.user.server.dao.Profile; import org.eclipse.che.api.user.server.dao.User; import org.eclipse.che.api.user.server.dao.UserDao; import org.eclipse.che.api.user.server.dao.UserProfileDao; import org.eclipse.che.api.workspace.server.dao.Member; import org.eclipse.che.api.workspace.server.dao.MemberDao; import org.eclipse.che.api.workspace.server.dao.Workspace; import org.eclipse.che.api.workspace.server.dao.WorkspaceDao; import org.eclipse.che.api.workspace.shared.dto.MemberDescriptor; import org.eclipse.che.api.workspace.shared.dto.NewMembership; import org.eclipse.che.api.workspace.shared.dto.NewWorkspace; import org.eclipse.che.api.workspace.shared.dto.WorkspaceDescriptor; import org.eclipse.che.api.workspace.shared.dto.WorkspaceReference; import org.eclipse.che.api.workspace.shared.dto.WorkspaceUpdate; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.dto.server.DtoFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; 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.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import static java.lang.Boolean.parseBoolean; import static java.util.Arrays.asList; import static java.util.Collections.singletonMap; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.Response.Status.CREATED; import static javax.ws.rs.core.Response.status; import static org.eclipse.che.api.project.server.Constants.LINK_REL_GET_PROJECTS; import static org.eclipse.che.api.user.server.Constants.LINK_REL_GET_USER_BY_ID; import static org.eclipse.che.commons.lang.NameGenerator.generate; /** * Workspace API * * @author Eugene Voevodin * @author Max Shaposhnik */ @Api(value = "/workspace", description = "Workspace manager") @Path("/workspace") public class WorkspaceService extends Service { private static final Logger LOG = LoggerFactory.getLogger(WorkspaceService.class); private final WorkspaceDao workspaceDao; private final UserDao userDao; private final MemberDao memberDao; private final UserProfileDao profileDao; private final PreferenceDao preferenceDao; private final AccountDao accountDao; @Inject public WorkspaceService(WorkspaceDao workspaceDao, UserDao userDao, MemberDao memberDao, AccountDao accountDao, UserProfileDao profileDao, PreferenceDao preferenceDao ) { this.workspaceDao = workspaceDao; this.userDao = userDao; this.memberDao = memberDao; this.accountDao = accountDao; this.profileDao = profileDao; this.preferenceDao = preferenceDao; } /** * Creates new workspace and adds current user as member to created workspace * with roles <i>"workspace/admin"</i> and <i>"workspace/developer"</i>. Returns status code <strong>201 CREATED</strong> * and {@link org.eclipse.che.api.workspace.shared.dto.WorkspaceDescriptor} if workspace has been created successfully. * Each new workspace should contain at least name and account identifier. * * @param newWorkspace * new workspace * @return descriptor of created workspace * @throws org.eclipse.che.api.core.ConflictException * when current user account identifier and given account identifier are different * @throws org.eclipse.che.api.core.NotFoundException * when account with given identifier does not exist * @throws org.eclipse.che.api.core.ServerException * when some error occurred while retrieving/persisting account, workspace or member * @throws org.eclipse.che.api.core.ForbiddenException * when user has not access to create workspaces, * or when new workspace is {@code null}, * or any of workspace name or account id is {@code null} * @see org.eclipse.che.api.workspace.shared.dto.NewWorkspace * @see org.eclipse.che.api.workspace.shared.dto.WorkspaceDescriptor * @see #getById(String, javax.ws.rs.core.SecurityContext) * @see #getByName(String, javax.ws.rs.core.SecurityContext) */ @ApiOperation(value = "Create a new workspace", response = WorkspaceDescriptor.class, position = 2) @ApiResponses(value = { @ApiResponse(code = 201, message = "CREATED"), @ApiResponse(code = 403, message = "You have no access to create more workspaces"), @ApiResponse(code = 404, message = "NOT FOUND"), @ApiResponse(code = 409, message = "You can create workspace associated only to your own account"), @ApiResponse(code = 500, message = "INTERNAL SERVER ERROR")}) @POST @GenerateLink(rel = Constants.LINK_REL_CREATE_WORKSPACE) @RolesAllowed({"user", "system/admin"}) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) public Response create(@ApiParam(value = "new workspace", required = true) @Required @Description("new workspace") NewWorkspace newWorkspace, @Context SecurityContext context) throws ConflictException, NotFoundException, ServerException, ForbiddenException { requiredNotNull(newWorkspace, "New workspace"); requiredNotNull(newWorkspace.getAccountId(), "Account ID"); if (newWorkspace.getAttributes() != null) { validateAttributes(newWorkspace.getAttributes()); } if (newWorkspace.getName() == null || newWorkspace.getName().isEmpty()) { newWorkspace.setName(generateWorkspaceName()); } final Account account = accountDao.getById(newWorkspace.getAccountId()); //check user has access to add new workspace if (!context.isUserInRole("system/admin")) { ensureCurrentUserOwnerOf(account); } if (account.getAttributes().containsKey(org.eclipse.che.api.account.server.Constants.RESOURCES_LOCKED_PROPERTY)) { newWorkspace.getAttributes().put(org.eclipse.che.api.account.server.Constants.RESOURCES_LOCKED_PROPERTY, "true"); } final Workspace workspace = new Workspace().withId(generate(Workspace.class.getSimpleName().toLowerCase(), Constants.ID_LENGTH)) .withName(newWorkspace.getName()) .withTemporary(false) .withAccountId(newWorkspace.getAccountId()) .withAttributes(newWorkspace.getAttributes()); workspaceDao.create(workspace); LOG.info("EVENT#workspace-created# WS#{}# WS-ID#{}# USER#{}#", newWorkspace.getName(), workspace.getId(), currentUser().getId()); return status(CREATED).entity(toDescriptor(workspace, context)).build(); } /** * Creates new temporary workspace and adds current user * as member to created workspace with roles <i>"workspace/admin"</i> and <i>"workspace/developer"</i>. * If user does not exist, it will be created with role <i>"tmp_user"</i>. * Returns status code <strong>201 CREATED</strong> and {@link WorkspaceDescriptor} if workspace * has been created successfully. Each new workspace should contain * at least workspace name and account identifier. * * @param newWorkspace * new workspace * @return descriptor of created workspace * @throws ConflictException * when current user account identifier and given account identifier are different * @throws ForbiddenException * when new workspace is {@code null}, * or any of workspace name or account identifier is {@code null} * @throws NotFoundException * when account with given identifier does not exist * @throws ServerException * when some error occurred while retrieving/persisting account, workspace, member or profile * @see WorkspaceDescriptor * @see #getById(String, SecurityContext) * @see #getByName(String, SecurityContext) */ @ApiOperation(value = "Create a temporary workspace", notes = "Create a temporary workspace created by a Factory", response = WorkspaceDescriptor.class, position = 1) @ApiResponses(value = { @ApiResponse(code = 201, message = "CREATED"), @ApiResponse(code = 403, message = "You have no access to create more workspaces"), @ApiResponse(code = 404, message = "NOT FOUND"), @ApiResponse(code = 409, message = "You can create workspace associated only to your own account"), @ApiResponse(code = 500, message = "INTERNAL SERVER ERROR") }) @POST @Path("/temp") @GenerateLink(rel = Constants.LINK_REL_CREATE_TEMP_WORKSPACE) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @SuppressWarnings("static-access") public Response createTemporary(@ApiParam(value = "New Temporary workspace", required = true) @Required @Description("New temporary workspace") NewWorkspace newWorkspace, @Context SecurityContext context) throws ConflictException, NotFoundException, ForbiddenException, ServerException { requiredNotNull(newWorkspace, "New workspace"); if (newWorkspace.getAttributes() != null) { validateAttributes(newWorkspace.getAttributes()); } final Workspace workspace = new Workspace().withId(generate(Workspace.class.getSimpleName().toLowerCase(), Constants.ID_LENGTH)) .withName(newWorkspace.getName()) .withTemporary(true) .withAccountId(newWorkspace.getAccountId()) .withAttributes(newWorkspace.getAttributes()); //temporary user should be created if real user does not exist final User user; boolean isTemporary = false; if (context.getUserPrincipal() == null) { user = createTemporaryUser(); isTemporary = true; } else { user = userDao.getById(currentUser().getId()); } if (!isTemporary && !context.isUserInRole("system/admin")) { final Account account = accountDao.getById(newWorkspace.getAccountId()); ensureCurrentUserOwnerOf(account); } createTemporaryWorkspace(workspace); final Member newMember = new Member().withUserId(user.getId()) .withWorkspaceId(workspace.getId()) .withRoles(asList("workspace/developer", "workspace/admin")); memberDao.create(newMember); LOG.info("EVENT#workspace-created# WS#{}# WS-ID#{}# USER#{}#", workspace.getName(), workspace.getId(), user.getId()); return status(CREATED).entity(toDescriptor(workspace, context)).build(); } /** * Searches for workspace with given identifier and returns {@link WorkspaceDescriptor} if found. * If user that has called this method is not <i>"workspace/admin"</i> or <i>"workspace/developer"</i> * workspace attributes will not be added to response. * * @param id * workspace identifier * @return descriptor of found workspace * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving workspace * @see WorkspaceDescriptor * @see #getByName(String, SecurityContext) */ @ApiOperation(value = "Get workspace by ID", response = WorkspaceDescriptor.class, position = 5) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Workspace with specified ID does not exist"), @ApiResponse(code = 403, message = "Access to requested workspace is forbidden"), @ApiResponse(code = 500, message = "Server error")}) @GET @Path("/{id}") @Produces(APPLICATION_JSON) public WorkspaceDescriptor getById(@ApiParam(value = "Workspace ID") @Description("Workspace ID") @PathParam("id") String id, @Context SecurityContext context) throws NotFoundException, ServerException, ForbiddenException { final Workspace workspace = workspaceDao.getById(id); if (!context.isUserInRole("account/owner") && !context.isUserInRole("workspace/developer") && !context.isUserInRole("workspace/admin")) { // tmp_workspace_cloned_from_private_repo - gives information // whether workspace was clone from private repository or not. It can be use // by temporary workspace sharing filter for user that are not workspace/admin // so we need that property here. // PLZ DO NOT REMOVE!!!! final Map<String, String> attributes = workspace.getAttributes(); if (attributes.containsKey("allowAnyoneAddMember")) { workspace.setAttributes(singletonMap("allowAnyoneAddMember", attributes.get("allowAnyoneAddMember"))); } else { attributes.clear(); } } return toDescriptor(workspace, context); } /** * Searches for workspace with given name and return {@link WorkspaceDescriptor} for it. * If user that has called this method is not <i>"workspace/admin"</i> or <i>"workspace/developer"</i> * workspace attributes will not be added to response. * * @param name * workspace name * @return descriptor of found workspace * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving workspace * @see WorkspaceDescriptor * @see #getById(String, SecurityContext) */ @ApiOperation(value = "Gets workspace by name", response = WorkspaceDescriptor.class, position = 4) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Workspace with specified name doesn't exist"), @ApiResponse(code = 403, message = "Access to requested workspace is forbidden"), @ApiResponse(code = 500, message = "Server error")}) @GET @GenerateLink(rel = Constants.LINK_REL_GET_WORKSPACE_BY_NAME) @Produces(APPLICATION_JSON) public WorkspaceDescriptor getByName(@ApiParam(value = "Name of workspace", required = true) @Required @Description("Name of workspace") @QueryParam("name") String name, @Context SecurityContext context) throws NotFoundException, ServerException, ForbiddenException { requiredNotNull(name, "Workspace name"); final Workspace workspace = workspaceDao.getByName(name); if (!context.isUserInRole("account/owner") && !context.isUserInRole("workspace/developer") && !context.isUserInRole("workspace/admin")) { // tmp_workspace_cloned_from_private_repo - gives information // whether workspace was clone from private repository or not. It can be use // by temporary workspace sharing filter for user that are not workspace/admin // so we need that property here. // PLZ DO NOT REMOVE!!!! final Map<String, String> attributes = workspace.getAttributes(); if (attributes.containsKey("allowAnyoneAddMember")) { workspace.setAttributes(singletonMap("allowAnyoneAddMember", attributes.get("allowAnyoneAddMember"))); } else { attributes.clear(); } } return toDescriptor(workspace, context); } /** * <p>Updates workspace.</p> * <strong>Note:</strong> existed workspace attributes with same name as * update attributes will be replaced with update attributes. * * @param id * workspace identifier * @param update * workspace update * @return descriptor of updated workspace * @throws NotFoundException * when workspace with given name doesn't exist * @throws ConflictException * when attribute with not valid name * @throws ForbiddenException * when update is {@code null} or updated attributes contains * @throws ServerException * when some error occurred while retrieving/updating workspace * @see WorkspaceUpdate * @see WorkspaceDescriptor * @see #removeAttribute(String, String, SecurityContext) */ @ApiOperation(value = "Update workspace", response = WorkspaceDescriptor.class, notes = "Update an existing workspace. A JSON with updated properties is sent.", position = 3) @ApiResponses(value = { @ApiResponse(code = 200, message = "Workspace updated"), @ApiResponse(code = 404, message = "Not found"), @ApiResponse(code = 403, message = "Access to required workspace is forbidden"), @ApiResponse(code = 500, message = "Internal server error")}) @POST @Path("/{id}") @RolesAllowed({"account/owner", "workspace/admin", "system/admin"}) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) public WorkspaceDescriptor update(@ApiParam(value = "Workspace ID") @PathParam("id") String id, @ApiParam(value = "Workspace update", required = true) @Required WorkspaceUpdate update, @Context SecurityContext context) throws NotFoundException, ConflictException, ForbiddenException, ServerException { requiredNotNull(update, "Workspace update"); final Workspace workspace = workspaceDao.getById(id); final Map<String, String> attributes = update.getAttributes(); if (attributes != null) { validateAttributes(attributes); workspace.getAttributes().putAll(attributes); } final String newName = update.getName(); if (newName != null) { workspace.setName(newName); } workspaceDao.update(workspace); LOG.info("EVENT#workspace-updated# WS#{}# WS-ID#{}#", workspace.getName(), workspace.getId()); return toDescriptor(workspace, context); } /** * Returns workspace descriptors for certain workspaces with given account identifier. * * @param accountId * account identifier * @return workspaces descriptors * @throws ForbiddenException * when account identifier is {@code null} * @throws ServerException * when some error occurred while retrieving workspace * @see WorkspaceDescriptor */ @ApiOperation(value = "Get workspace by Account ID", notes = "Search for a workspace by its Account ID which is added as query parameter", response = WorkspaceDescriptor.class, responseContainer = "List", position = 6) @ApiResponses(value = { @ApiResponse(code = 403, message = "User is not authorized to call this operation"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/find/account") @GenerateLink(rel = Constants.LINK_REL_GET_WORKSPACES_BY_ACCOUNT) @RolesAllowed({"user", "system/admin", "system/manager"}) @Produces(APPLICATION_JSON) public List<WorkspaceDescriptor> getWorkspacesByAccount(@ApiParam(value = "Account ID", required = true) @Required @QueryParam("id") String accountId, @Context SecurityContext context) throws ServerException, ForbiddenException { requiredNotNull(accountId, "Account ID"); final List<Workspace> workspaces = workspaceDao.getByAccount(accountId); final List<WorkspaceDescriptor> descriptors = new ArrayList<>(workspaces.size()); for (Workspace workspace : workspaces) { descriptors.add(toDescriptor(workspace, context)); } return descriptors; } /** * Returns all memberships of current user. * * @return current user memberships * @throws ServerException * when some error occurred while retrieving user or members * @see MemberDescriptor */ @ApiOperation(value = "Get membership of a current user", notes = "Get membership and workspace roles of a current user", response = MemberDescriptor.class, responseContainer = "List", position = 9) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/all") @GenerateLink(rel = Constants.LINK_REL_GET_CURRENT_USER_WORKSPACES) @RolesAllowed({"user", "temp_user"}) @Produces(APPLICATION_JSON) public List<MemberDescriptor> getMembershipsOfCurrentUser(@Context SecurityContext context) throws NotFoundException, ServerException { final List<Member> members = memberDao.getUserRelationships(currentUser().getId()); final List<MemberDescriptor> memberships = new ArrayList<>(members.size()); for (Member member : members) { try { final Workspace workspace = workspaceDao.getById(member.getWorkspaceId()); memberships.add(toDescriptor(member, workspace, context)); } catch (NotFoundException nfEx) { LOG.error("Workspace {} doesn't exist but user {} refers to it. ", member.getWorkspaceId(), currentUser().getId()); } } return memberships; } /** * Returns all memberships of certain user. * * @param userId * user identifier to search memberships * @return certain user memberships * @throws NotFoundException * when user with given identifier doesn't exist * @throws ForbiddenException * when user identifier is {@code null} * @throws ServerException * when some error occurred while retrieving user or members * @see MemberDescriptor */ @ApiOperation(value = "Get memberships by user ID", notes = "Search for a workspace by User ID which is added to URL as query parameter. JSON with workspace details and user roles is returned", response = MemberDescriptor.class, responseContainer = "List", position = 7) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 403, message = "User not authorized to call this action"), @ApiResponse(code = 404, message = "Not Foound"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/find") @GenerateLink(rel = Constants.LINK_REL_GET_CONCRETE_USER_WORKSPACES) @RolesAllowed({"system/admin", "system/manager"}) @Produces(APPLICATION_JSON) public List<MemberDescriptor> getMembershipsOfSpecificUser(@ApiParam(value = "User ID", required = true) @Required @QueryParam("userid") String userId, @Context SecurityContext context) throws NotFoundException, ForbiddenException, ServerException { requiredNotNull(userId, "User ID"); final List<Member> members = memberDao.getUserRelationships(userId); final List<MemberDescriptor> memberships = new ArrayList<>(members.size()); for (Member member : members) { try { final Workspace workspace = workspaceDao.getById(member.getWorkspaceId()); memberships.add(toDescriptor(member, workspace, context)); } catch (NotFoundException nfEx) { LOG.error("Workspace {} doesn't exist but user {} refers to it. ", member.getWorkspaceId(), userId); } } return memberships; } /** * Returns all workspace members. * * @param wsId * workspace identifier * @return workspace members * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving workspace or members * @see MemberDescriptor * @see #addMember(String, NewMembership, SecurityContext) * @see #removeMember(String, String, SecurityContext) */ @ApiOperation(value = "Get workspace members by workspace ID", notes = "Get all workspace members of a specified workspace. A JSOn with members and their roles is returned", response = MemberDescriptor.class, responseContainer = "List", position = 8) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/{id}/members") @RolesAllowed({"workspace/admin", "workspace/developer", "account/owner", "system/admin", "system/manager"}) @Produces(APPLICATION_JSON) public List<MemberDescriptor> getMembers(@ApiParam(value = "Workspace ID") @PathParam("id") String wsId, @Context SecurityContext context) throws NotFoundException, ServerException, ForbiddenException { final Workspace workspace = workspaceDao.getById(wsId); final List<Member> members = memberDao.getWorkspaceMembers(wsId); final List<MemberDescriptor> descriptors = new ArrayList<>(members.size()); for (Member member : members) { descriptors.add(toDescriptor(member, workspace, context)); } return descriptors; } /** * Returns membership for current user in the given workspace. * * @param wsId * workspace identifier * @return workspace member * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving workspace or members * @see MemberDescriptor * @see #addMember(String, NewMembership, SecurityContext) * @see #removeMember(String, String, SecurityContext) */ @ApiOperation(value = "Get user membership in a specified workspace", notes = "Returns membership of a user with roles", response = MemberDescriptor.class, position = 10) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/{id}/membership") @RolesAllowed({"workspace/stakeholder", "workspace/developer", "workspace/admin"}) @Produces(APPLICATION_JSON) public MemberDescriptor getMembershipOfCurrentUser(@ApiParam(value = "Workspace ID") @PathParam("id") String wsId, @Context SecurityContext context) throws NotFoundException, ServerException { final Workspace workspace = workspaceDao.getById(wsId); final Member member = memberDao.getWorkspaceMember(wsId, currentUser().getId()); return toDescriptor(member, workspace, context); } /** * Removes attribute from certain workspace. * * @param wsId * workspace identifier * @param attributeName * attribute name to remove * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while getting or updating workspace * @throws ConflictException * when given attribute name is not valid */ @ApiOperation(value = "Delete workspace attribute", notes = "Deletes attributes of a specified workspace", position = 11) @ApiResponses(value = { @ApiResponse(code = 204, message = "No Content"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "Invalid attribute name"), @ApiResponse(code = 500, message = "Internal Server Error")}) @DELETE @Path("/{id}/attribute") @RolesAllowed({"account/owner", "workspace/admin", "system/admin"}) public void removeAttribute(@ApiParam(value = "Workspace ID") @PathParam("id") String wsId, @ApiParam(value = "Attribute2 name", required = true) @Required @QueryParam("name") String attributeName, @Context SecurityContext context) throws NotFoundException, ServerException, ConflictException { validateAttributeName(attributeName); final Workspace workspace = workspaceDao.getById(wsId); if (null != workspace.getAttributes().remove(attributeName)) { workspaceDao.update(workspace); } } /** * Creates new workspace member. * * @param wsId * workspace identifier * @param newMembership * new membership * @return descriptor of created member * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving {@link Workspace}, {@link org.eclipse.che.api.user.shared.dto.UserDescriptor} * or persisting new {@link Member} * @throws ConflictException * when new membership is {@code null} * or if new membership user id is {@code null} or * of new membership roles is {@code null} or empty * @throws ForbiddenException * when current user hasn't access to workspace with given identifier * @see MemberDescriptor * @see #removeMember(String, String, SecurityContext) * @see #getMembers(String, SecurityContext) */ @ApiOperation(value = "Create new workspace member", notes = "Add a new member into a workspace", response = MemberDescriptor.class, position = 12) @ApiResponses(value = { @ApiResponse(code = 201, message = "OK"), @ApiResponse(code = 403, message = "User not authorized to perform this operation"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "No user ID and/or role specified")}) @POST @Path("/{id}/members") @RolesAllowed({"user", "temp_user"}) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) public Response addMember(@ApiParam(value = "Workspace ID") @PathParam("id") String wsId, @ApiParam(value = "New membership", required = true) @Required NewMembership newMembership, @Context SecurityContext context) throws NotFoundException, ServerException, ConflictException, ForbiddenException { requiredNotNull(newMembership, "New membership"); requiredNotNull(newMembership.getUserId(), "User ID"); final Workspace workspace = workspaceDao.getById(wsId); if (memberDao.getWorkspaceMembers(wsId).isEmpty()) { //if workspace doesn't contain members then member that is been added //should be added with roles 'workspace/admin' and 'workspace/developer' newMembership.setRoles(asList("workspace/admin", "workspace/developer")); } else { requiredNotNull(newMembership.getRoles(), "Roles"); if (newMembership.getRoles().isEmpty()) { throw new ConflictException("Roles should not be empty"); } if (!context.isUserInRole("workspace/admin") && !parseBoolean(workspace.getAttributes().get("allowAnyoneAddMember")) && !isCurrentUserAccountOwnerOf(wsId)) { throw new ForbiddenException("Access denied"); } } final User user = userDao.getById(newMembership.getUserId()); final Member newMember = new Member().withWorkspaceId(wsId) .withUserId(user.getId()) .withRoles(newMembership.getRoles()); memberDao.create(newMember); return status(CREATED).entity(toDescriptor(newMember, workspace, context)).build(); } /** * Removes user with given identifier as member from certain workspace. * * @param wsId * workspace identifier * @param userId * user identifier to remove member * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving workspace or removing member * @throws ConflictException * when removal member is last <i>"workspace/admin"</i> in given workspace * @see #addMember(String, NewMembership, SecurityContext) * @see #getMembers(String, SecurityContext) */ @ApiOperation(value = "Remove user from workspace", notes = "Remove a user from a workspace by User ID", position = 13) @ApiResponses(value = { @ApiResponse(code = 204, message = "No Content"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "Cannot remove workspace/admin"), @ApiResponse(code = 500, message = "Internal Server Error")}) @DELETE @Path("/{id}/members/{userid}") @RolesAllowed({"account/owner", "workspace/admin", "account/owner"}) public void removeMember(@ApiParam(value = "Workspace ID") @PathParam("id") String wsId, @ApiParam(value = "User ID") @PathParam("userid") String userId, @Context SecurityContext context) throws NotFoundException, ServerException, ConflictException { memberDao.remove(new Member().withUserId(userId).withWorkspaceId(wsId)); } /** * Removes certain workspace. * BTW all workspace members are going to be removed as well. * * @param wsId * workspace identifier to remove workspace * @throws NotFoundException * when workspace with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving/removing workspace or member * @throws ConflictException * if some error occurred while removing member */ @ApiOperation(value = "Delete a workspace", notes = "Delete a workspace by its ID", position = 14) @ApiResponses(value = { @ApiResponse(code = 204, message = "No Content"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "Failed to remove workspace member"), @ApiResponse(code = 500, message = "Internal Server Error")}) @DELETE @Path("/{id}") @RolesAllowed({"account/owner", "workspace/admin", "system/admin"}) public void remove(@ApiParam(value = "Workspace ID") @PathParam("id") String wsId) throws NotFoundException, ServerException, ConflictException { workspaceDao.remove(wsId); } private void createTemporaryWorkspace(Workspace workspace) throws ConflictException, ServerException { try { //let vfs create temporary workspace in correct place EnvironmentContext.getCurrent().setWorkspaceTemporary(true); workspaceDao.create(workspace); } finally { EnvironmentContext.getCurrent().setWorkspaceTemporary(false); } } private User createTemporaryUser() throws ConflictException, ServerException, NotFoundException { final String id = generate("tmp_user", org.eclipse.che.api.user.server.Constants.ID_LENGTH); //creating user final User user = new User().withId(id); userDao.create(user); //creating profile for it profileDao.create(new Profile().withId(id).withUserId(id)); //storing preferences final Map<String, String> preferences = new HashMap<>(4); preferences.put("temporary", String.valueOf(true)); preferences.put("codenvy:created", Long.toString(System.currentTimeMillis())); preferenceDao.setPreferences(id, preferences); return user; } /** * Converts {@link Member} to {@link MemberDescriptor} */ /* used in tests */MemberDescriptor toDescriptor(Member member, Workspace workspace, SecurityContext context) { final UriBuilder serviceUriBuilder = getServiceContext().getServiceUriBuilder(); final UriBuilder baseUriBuilder = getServiceContext().getBaseUriBuilder(); final List<Link> links = new LinkedList<>(); if (context.isUserInRole("account/owner") || context.isUserInRole("workspace/admin") || context.isUserInRole("workspace/developer")) { links.add(LinksHelper.createLink("GET", serviceUriBuilder.clone() .path(getClass(), "getMembers") .build(workspace.getId()) .toString(), null, APPLICATION_JSON, Constants.LINK_REL_GET_WORKSPACE_MEMBERS)); } if (context.isUserInRole("account/owner") || context.isUserInRole("workspace/admin")) { links.add(LinksHelper.createLink("DELETE", serviceUriBuilder.clone() .path(getClass(), "removeMember") .build(workspace.getId(), member.getUserId()) .toString(), null, null, Constants.LINK_REL_REMOVE_WORKSPACE_MEMBER)); } links.add(LinksHelper.createLink("GET", baseUriBuilder.clone() .path(UserService.class) .path(UserService.class, "getById") .build(member.getUserId()) .toString(), null, APPLICATION_JSON, LINK_REL_GET_USER_BY_ID)); final Link wsLink = LinksHelper.createLink("GET", serviceUriBuilder.clone() .path(getClass(), "getById") .build(workspace.getId()) .toString(), null, APPLICATION_JSON, Constants.LINK_REL_GET_WORKSPACE_BY_ID); final Link projectsLink = LinksHelper.createLink("GET", baseUriBuilder.clone() .path(ProjectService.class) .path(ProjectService.class, "getProjects") .build(workspace.getId()) .toString(), null, APPLICATION_JSON, LINK_REL_GET_PROJECTS); final WorkspaceReference wsRef = DtoFactory.getInstance().createDto(WorkspaceReference.class) .withId(workspace.getId()) .withName(workspace.getName()) .withTemporary(workspace.isTemporary()) .withLinks(asList(wsLink, projectsLink)); return DtoFactory.getInstance().createDto(MemberDescriptor.class) .withUserId(member.getUserId()) .withWorkspaceReference(wsRef) .withRoles(member.getRoles()) .withLinks(links); } /** * Converts {@link Workspace} to {@link WorkspaceDescriptor} */ /* used in tests */WorkspaceDescriptor toDescriptor(Workspace workspace, SecurityContext context) { final WorkspaceDescriptor workspaceDescriptor = DtoFactory.getInstance().createDto(WorkspaceDescriptor.class) .withId(workspace.getId()) .withName(workspace.getName()) .withTemporary(workspace.isTemporary()) .withAccountId(workspace.getAccountId()) .withAttributes(workspace.getAttributes()); final List<Link> links = new LinkedList<>(); final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder(); if (context.isUserInRole("user")) { links.add(LinksHelper.createLink("GET", getServiceContext().getBaseUriBuilder().clone() .path(ProjectService.class) .path(ProjectService.class, "getProjects") .build(workspaceDescriptor.getId()) .toString(), null, APPLICATION_JSON, org.eclipse.che.api.project.server.Constants.LINK_REL_GET_PROJECTS)); links.add(LinksHelper.createLink("GET", uriBuilder.clone() .path(getClass(), "getMembershipsOfCurrentUser") .build() .toString(), null, APPLICATION_JSON, Constants.LINK_REL_GET_CURRENT_USER_WORKSPACES)); links.add(LinksHelper.createLink("GET", uriBuilder.clone() .path(getClass(), "getMembershipOfCurrentUser") .build(workspaceDescriptor.getId()) .toString(), null, APPLICATION_JSON, Constants.LINK_REL_GET_CURRENT_USER_MEMBERSHIP)); } if (context.isUserInRole("workspace/admin") || context.isUserInRole("workspace/developer") || context.isUserInRole("system/admin") || context.isUserInRole("system/manager") || context.isUserInRole("account/owner")) { links.add(LinksHelper.createLink("GET", uriBuilder.clone(). path(getClass(), "getByName") .queryParam("name", workspaceDescriptor.getName()) .build() .toString(), null, APPLICATION_JSON, Constants.LINK_REL_GET_WORKSPACE_BY_NAME)); links.add(LinksHelper.createLink("GET", uriBuilder.clone() .path(getClass(), "getById") .build(workspaceDescriptor.getId()) .toString(), null, APPLICATION_JSON, Constants.LINK_REL_GET_WORKSPACE_BY_ID)); links.add(LinksHelper.createLink("GET", uriBuilder.clone() .path(getClass(), "getMembers") .build(workspaceDescriptor.getId()) .toString(), null, APPLICATION_JSON, Constants.LINK_REL_GET_WORKSPACE_MEMBERS)); } if (context.isUserInRole("account/owner") || context.isUserInRole("workspace/admin") || context.isUserInRole("system/admin")) { links.add(LinksHelper.createLink("DELETE", uriBuilder.clone() .path(getClass(), "remove") .build(workspaceDescriptor.getId()) .toString(), null, null, Constants.LINK_REL_REMOVE_WORKSPACE)); } return workspaceDescriptor.withLinks(links); } /** * Checks object reference is not {@code null} * * @param object * object reference to check * @param subject * used as subject of exception message "{subject} required" * @throws ForbiddenException * when object reference is {@code null} */ private void requiredNotNull(Object object, String subject) throws ForbiddenException { if (object == null) { throw new ForbiddenException(subject + " required"); } } /** * Validates attribute name. * * @param attributeName * attribute name to check * @throws ConflictException * when attribute name is {@code null}, empty or it starts with "codenvy" */ private void validateAttributeName(String attributeName) throws ConflictException { if (attributeName == null || attributeName.isEmpty() || attributeName.toLowerCase().startsWith("codenvy")) { throw new ConflictException(String.format("Attribute2 name '%s' is not valid", attributeName)); } } private void validateAttributes(Map<String, String> attributes) throws ConflictException { for (String attributeName : attributes.keySet()) { validateAttributeName(attributeName); } } private void ensureCurrentUserOwnerOf(Account target) throws ServerException, NotFoundException, ConflictException { final List<Account> accounts = accountDao.getByOwner(currentUser().getId()); for (Account account : accounts) { if (account.getId().equals(target.getId())) { return; } } throw new ConflictException("You can create workspace associated only with your own account"); } private boolean isCurrentUserAccountOwnerOf(String wsId) throws ServerException, NotFoundException { final List<Account> accounts = accountDao.getByOwner(currentUser().getId()); final List<Workspace> workspaces = new LinkedList<>(); //fetch all workspaces related to accounts for (Account account : accounts) { workspaces.addAll(workspaceDao.getByAccount(account.getId())); } for (Workspace workspace : workspaces) { if (workspace.getId().equals(wsId)) { return true; } } return false; } /** * Generates workspace name based on current user email. * Generating process is simple, assuming we have user with email user@codenvy.com, * then first time we will check for workspace with name equal to "user" and if it is free * it will be returned, but if it is reserved then number suffix will be added to the end of "user" name * and it will be checked again until free workspace name is not found. */ private String generateWorkspaceName() throws ServerException { //should be email String userName = currentUser().getName(); int atIdx = userName.indexOf('@'); //if username contains email then fetch part before '@' if (atIdx != -1) { userName = userName.substring(0, atIdx); } //search first workspace name which is free int suffix = 2; String workspaceName = userName; while (workspaceExists(workspaceName)) { workspaceName = userName + suffix++; } return workspaceName; } private boolean workspaceExists(String name) throws ServerException { try { workspaceDao.getByName(name); } catch (NotFoundException nfEx) { return false; } return true; } private org.eclipse.che.commons.user.User currentUser() { return EnvironmentContext.getCurrent().getUser(); } }