/******************************************************************************* * 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.account.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.account.server.dao.Member; import org.eclipse.che.api.account.server.dao.PlanDao; import org.eclipse.che.api.account.server.dao.Subscription; import org.eclipse.che.api.account.shared.dto.AccountDescriptor; import org.eclipse.che.api.account.shared.dto.AccountReference; import org.eclipse.che.api.account.shared.dto.AccountUpdate; import org.eclipse.che.api.account.shared.dto.MemberDescriptor; import org.eclipse.che.api.account.shared.dto.NewAccount; import org.eclipse.che.api.account.shared.dto.NewMembership; import org.eclipse.che.api.account.shared.dto.NewSubscription; import org.eclipse.che.api.account.shared.dto.NewSubscriptionTemplate; import org.eclipse.che.api.account.shared.dto.Plan; import org.eclipse.che.api.account.shared.dto.SubscriptionDescriptor; import org.eclipse.che.api.account.shared.dto.SubscriptionReference; import org.eclipse.che.api.account.shared.dto.SubscriptionResourcesUsed; import org.eclipse.che.api.account.shared.dto.SubscriptionState; import org.eclipse.che.api.account.shared.dto.UpdateResourcesDescriptor; import org.eclipse.che.api.account.shared.dto.UsedAccountResources; import org.eclipse.che.api.core.ApiException; 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.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.user.server.dao.User; import org.eclipse.che.api.user.server.dao.UserDao; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.lang.NameGenerator; 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.HttpMethod; 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.UriBuilder; import java.security.Principal; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import static java.lang.String.format; import static java.util.Collections.singletonList; /** * Account API * * @author Eugene Voevodin * @author Alex Garagatyi */ @Api(value = "/account", description = "Account manager") @Path("/account") public class AccountService extends Service { private static final Logger LOG = LoggerFactory.getLogger(AccountService.class); private final AccountDao accountDao; private final UserDao userDao; private final SubscriptionServiceRegistry registry; private final PlanDao planDao; private final ResourcesManager resourcesManager; @Inject public AccountService(AccountDao accountDao, UserDao userDao, SubscriptionServiceRegistry registry, PlanDao planDao, ResourcesManager resourcesManager) { this.accountDao = accountDao; this.userDao = userDao; this.registry = registry; this.planDao = planDao; this.resourcesManager = resourcesManager; } /** * Creates new account and adds current user as member to created account * with role <i>"account/owner"</i>. Returns status <b>201 CREATED</b> * and {@link AccountDescriptor} of created account if account has been created successfully. * Each new account should contain at least name. * * @param newAccount * new account * @return descriptor of created account * @throws NotFoundException * when some error occurred while retrieving account * @throws ConflictException * when new account is {@code null} * or new account name is {@code null} * or when any of new account attributes is not valid * @throws ServerException * @see AccountDescriptor * @see #getById(String, SecurityContext) * @see #getByName(String, SecurityContext) */ @ApiOperation(value = "Create a new account", notes = "Create a new account", response = Account.class, position = 1) @ApiResponses(value = { @ApiResponse(code = 201, message = "CREATED"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "Conflict Error"), @ApiResponse(code = 500, message = "Internal Server Error")}) @POST @GenerateLink(rel = Constants.LINK_REL_CREATE_ACCOUNT) @RolesAllowed({"user", "system/admin"}) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response create(@Context SecurityContext securityContext, @Required NewAccount newAccount) throws NotFoundException, ConflictException, ServerException { requiredNotNull(newAccount, "New account"); requiredNotNull(newAccount.getName(), "Account name"); if (newAccount.getAttributes() != null) { for (String attributeName : newAccount.getAttributes().keySet()) { validateAttributeName(attributeName); } } User current = null; if (securityContext.isUserInRole("user")) { current = userDao.getByAlias(securityContext.getUserPrincipal().getName()); //for now account <-One to One-> user if (accountDao.getByOwner(current.getId()).size() != 0) { throw new ConflictException(format("Account which owner is %s already exists", current.getId())); } } try { accountDao.getByName(newAccount.getName()); throw new ConflictException(format("Account with name %s already exists", newAccount.getName())); } catch (NotFoundException ignored) { } final String accountId = NameGenerator.generate(Account.class.getSimpleName().toLowerCase(), Constants.ID_LENGTH); final Account account = new Account().withId(accountId) .withName(newAccount.getName()) .withAttributes(newAccount.getAttributes()); accountDao.create(account); if (current != null) { final Member owner = new Member().withAccountId(accountId) .withUserId(current.getId()) .withRoles(Arrays.asList("account/owner")); accountDao.addMember(owner); } return Response.status(Response.Status.CREATED) .entity(toDescriptor(account, securityContext)) .build(); } /** * Returns all accounts memberships for current user. * * @return accounts memberships of current user * @throws NotFoundException * when any of memberships contains account that doesn't exist * @throws ServerException * when some error occurred while retrieving accounts or memberships * @see MemberDescriptor */ @ApiOperation(value = "Get current user memberships", notes = "This API call returns a JSON with all user membership in a single or multiple accounts", response = MemberDescriptor.class, responseContainer = "List", position = 2) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @GenerateLink(rel = Constants.LINK_REL_GET_ACCOUNTS) @RolesAllowed("user") @Produces(MediaType.APPLICATION_JSON) public List<MemberDescriptor> getMemberships(@Context SecurityContext securityContext) throws NotFoundException, ServerException { final Principal principal = securityContext.getUserPrincipal(); final User current = userDao.getByAlias(principal.getName()); final List<Member> memberships = accountDao.getByMember(current.getId()); final List<MemberDescriptor> result = new ArrayList<>(memberships.size()); for (Member membership : memberships) { result.add(toDescriptor(membership, accountDao.getById(membership.getAccountId()), securityContext)); } return result; } /** * Returns all accounts memberships for user with given identifier. * * @param userId * user identifier to search memberships * @return accounts memberships * @throws ConflictException * when user identifier is {@code null} * @throws NotFoundException * when user with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving user or memberships * @see MemberDescriptor */ @ApiOperation(value = "Get memberships of a specific user", notes = "ID of a user should be specified as a query parameter. JSON with membership details is returned. For this API call system/admin or system/manager role is required", response = MemberDescriptor.class, responseContainer = "List", position = 3) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "No User ID specified"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/memberships") @GenerateLink(rel = Constants.LINK_REL_GET_ACCOUNTS) @RolesAllowed({"system/admin", "system/manager"}) @Produces(MediaType.APPLICATION_JSON) public List<MemberDescriptor> getMembershipsOfSpecificUser(@ApiParam(value = "User ID", required = true) @Required @QueryParam("userid") String userId, @Context SecurityContext securityContext) throws NotFoundException, ServerException, ConflictException { requiredNotNull(userId, "User identifier"); final User user = userDao.getById(userId); final List<Member> memberships = accountDao.getByMember(user.getId()); final List<MemberDescriptor> result = new ArrayList<>(memberships.size()); for (Member membership : memberships) { result.add(toDescriptor(membership, accountDao.getById(membership.getAccountId()), securityContext)); } return result; } /** * Removes attribute with given name from certain account. * * @param accountId * account identifier * @param attributeName * attribute name to remove attribute * @throws ConflictException * if attribute name is not valid * @throws NotFoundException * if account with given identifier doesn't exist * @throws ServerException * when some error occurred while getting/updating account */ @ApiOperation(value = "Delete account attribute", notes = "Remove attribute from an account. Attribute name is used as a quary parameter. For this API request account/owner, system/admin or system/manager role is required", position = 4) @ApiResponses(value = { @ApiResponse(code = 204, message = "OK"), @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", "system/admin", "system/manager"}) public void removeAttribute(@ApiParam(value = "Account ID", required = true) @PathParam("id") String accountId, @ApiParam(value = "Attribute name to be removed", required = true) @QueryParam("name") String attributeName) throws ConflictException, NotFoundException, ServerException { validateAttributeName(attributeName); final Account account = accountDao.getById(accountId); account.getAttributes().remove(attributeName); accountDao.update(account); } /** * Searches for account with given identifier and returns {@link AccountDescriptor} for it. * * @param id * account identifier * @return descriptor of found account * @throws NotFoundException * when account with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving account * @see AccountDescriptor * @see #getByName(String, SecurityContext) */ @ApiOperation(value = "Get account by ID", notes = "Get account information by its ID. JSON with account details is returned. This API call requires account/owner, system/admin or system/manager role.", response = AccountDescriptor.class, position = 5) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/{id}") @RolesAllowed({"account/owner", "system/admin", "system/manager"}) @Produces(MediaType.APPLICATION_JSON) public AccountDescriptor getById(@ApiParam(value = "Account ID", required = true) @PathParam("id") String id, @Context SecurityContext securityContext) throws NotFoundException, ServerException { final Account account = accountDao.getById(id); return toDescriptor(account, securityContext); } /** * Searches for account with given name and returns {@link AccountDescriptor} for it. * * @param name * account name * @return descriptor of found account * @throws NotFoundException * when account with given name doesn't exist * @throws ConflictException * when account name is {@code null} * @throws ServerException * when some error occurred while retrieving account * @see AccountDescriptor * @see #getById(String, SecurityContext) */ @ApiOperation(value = "Get account by name", notes = "Get account information by its name. JSON with account details is returned. This API call requires system/admin or system/manager role.", response = AccountDescriptor.class, position = 5) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "No account name specified"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/find") @GenerateLink(rel = Constants.LINK_REL_GET_ACCOUNT_BY_NAME) @RolesAllowed({"system/admin", "system/manager"}) @Produces(MediaType.APPLICATION_JSON) public AccountDescriptor getByName(@ApiParam(value = "Account name", required = true) @Required @QueryParam("name") String name, @Context SecurityContext securityContext) throws NotFoundException, ServerException, ConflictException { requiredNotNull(name, "Account name"); final Account account = accountDao.getByName(name); return toDescriptor(account, securityContext); } /** * Creates new account member with role <i>"account/member"</i>. * * @param accountId * account identifier * @param membership * new membership * @return descriptor of created member * @throws ConflictException * when user identifier is {@code null} * @throws NotFoundException * when user or account with given identifier doesn't exist * @throws ServerException * when some error occurred while getting user or adding new account member * @see MemberDescriptor * @see #removeMember(String, String) * @see #getMembers(String, SecurityContext) */ @ApiOperation(value = "Add a new member to account", notes = "Add a new user to an account. This user will have account/member role. This API call requires account/owner, system/admin or system/manager role.", response = MemberDescriptor.class, position = 6) @ApiResponses(value = { @ApiResponse(code = 204, message = "OK"), @ApiResponse(code = 404, message = "Not Found"), @ApiResponse(code = 409, message = "No user ID specified"), @ApiResponse(code = 500, message = "Internal Server Error")}) @POST @Path("/{id}/members") @RolesAllowed({"account/owner", "system/admin"}) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response addMember(@ApiParam(value = "Account ID") @PathParam("id") String accountId, @ApiParam(value = "New membership", required = true) @Required NewMembership membership, @Context SecurityContext context) throws ConflictException, NotFoundException, ServerException { requiredNotNull(membership, "New membership"); requiredNotNull(membership.getUserId(), "User ID"); requiredNotNull(membership.getRoles(), "Roles"); if (membership.getRoles().isEmpty()) { throw new ConflictException("Roles should not be empty"); } userDao.getById(membership.getUserId());//check user exists final Member newMember = new Member().withAccountId(accountId) .withUserId(membership.getUserId()) .withRoles(membership.getRoles()); accountDao.addMember(newMember); return Response.status(Response.Status.CREATED) .entity(toDescriptor(newMember, accountDao.getById(accountId), context)) .build(); } /** * Returns all members of certain account. * * @param id * account identifier * @return account members * @throws NotFoundException * when account with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving accounts or members * @see MemberDescriptor * @see #addMember(String, NewMembership, SecurityContext) * @see #removeMember(String, String) */ @ApiOperation(value = "Get account members", notes = "Get all members for a specific account. This API call requires account/owner, system/admin or system/manager role.", response = MemberDescriptor.class, responseContainer = "List", position = 7) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Account ID not found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/{id}/members") @RolesAllowed({"account/owner", "system/admin", "system/manager"}) @Produces(MediaType.APPLICATION_JSON) public List<MemberDescriptor> getMembers(@ApiParam(value = "Account ID") @PathParam("id") String id, @Context SecurityContext securityContext) throws NotFoundException, ServerException { final Account account = accountDao.getById(id); final List<Member> members = accountDao.getMembers(id); final List<MemberDescriptor> result = new ArrayList<>(members.size()); for (Member member : members) { result.add(toDescriptor(member, account, securityContext)); } return result; } /** * Removes user with given identifier as member from certain account. * * @param accountId * account identifier * @param userId * user identifier * @throws NotFoundException * when user or account with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving account members or removing certain member * @throws ConflictException * when removal member is last <i>"account/owner"</i> * @see #addMember(String, NewMembership, SecurityContext) * @see #getMembers(String, SecurityContext) */ @ApiOperation(value = "Remove user from account", notes = "Remove user from a specific account. This API call requires account/owner, system/admin or system/manager role.", position = 8) @ApiResponses(value = { @ApiResponse(code = 204, message = "OK"), @ApiResponse(code = 404, message = "Account ID not found"), @ApiResponse(code = 409, message = "Account should have at least 1 owner"), @ApiResponse(code = 500, message = "Internal Server Error")}) @DELETE @Path("/{id}/members/{userid}") @RolesAllowed({"account/owner", "system/admin", "system/manager"}) public void removeMember(@ApiParam(value = "Account ID", required = true) @PathParam("id") String accountId, @ApiParam(value = "User ID") @PathParam("userid") String userId) throws NotFoundException, ServerException, ConflictException { final List<Member> members = accountDao.getMembers(accountId); //search for member Member target = null; int owners = 0; for (Member member : members) { if (member.getRoles().contains("account/owner")) owners++; if (member.getUserId().equals(userId)) target = member; } if (target == null) { throw new ConflictException(format("User %s doesn't have membership with account %s", userId, accountId)); } //account should have at least 1 owner if (owners == 1 && target.getRoles().contains("account/owner")) { throw new ConflictException("Account should have at least 1 owner"); } accountDao.removeMember(target); } /** * <p>Updates account.</p> * <strong>Note:</strong> existed account attributes with same names as * update attributes will be replaced with update attributes. * * @param accountId * account identifier * @param update * account update * @return descriptor of updated account * @throws NotFoundException * when account with given identifier doesn't exist * @throws ConflictException * when account update is {@code null} * or when account with given name already exists * @throws ServerException * when some error occurred while retrieving/persisting account * @see AccountDescriptor */ @ApiOperation(value = "Update account", notes = "Update account. This API call requires account/owner role.", response = AccountDescriptor.class, position = 9) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Account ID not found"), @ApiResponse(code = 409, message = "Invalid account ID or account name already exists"), @ApiResponse(code = 500, message = "Internal Server Error")}) @POST @Path("/{id}") @RolesAllowed({"account/owner"}) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public AccountDescriptor update(@ApiParam(value = "Account ID", required = true) @PathParam("id") String accountId, AccountUpdate update, @Context SecurityContext securityContext) throws NotFoundException, ConflictException, ServerException { requiredNotNull(update, "Account update"); final Account account = accountDao.getById(accountId); //current user should be account owner to update it if (update.getName() != null) { if (!account.getName().equals(update.getName()) && accountDao.getByName(update.getName()) != null) { throw new ConflictException(format("Account with name %s already exists", update.getName())); } else { account.setName(update.getName()); } } if (update.getAttributes() != null) { for (String attributeName : update.getAttributes().keySet()) { validateAttributeName(attributeName); } account.getAttributes().putAll(update.getAttributes()); } accountDao.update(account); return toDescriptor(account, securityContext); } /** * Returns list of subscriptions descriptors for certain account. * If service identifier is provided returns subscriptions that matches provided service. * * @param accountId * account identifier * @param serviceId * service identifier * @return subscriptions descriptors * @throws NotFoundException * when account with given identifier doesn't exist * @throws ServerException * when some error occurred while retrieving subscriptions * @see SubscriptionDescriptor */ @ApiOperation(value = "Get account subscriptions", notes = "Get information on account subscriptions. This API call requires account/owner, account/member, system/admin or system/manager role.", response = SubscriptionDescriptor.class, responseContainer = "List", position = 10) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Account ID not found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/{accountId}/subscriptions") @RolesAllowed({"account/member", "account/owner", "system/admin", "system/manager"}) @Produces(MediaType.APPLICATION_JSON) public List<SubscriptionDescriptor> getSubscriptions(@ApiParam(value = "Account ID", required = true) @PathParam("accountId") String accountId, @ApiParam(value = "Service ID", required = false) @QueryParam("service") String serviceId, @Context SecurityContext securityContext) throws NotFoundException, ServerException { final List<Subscription> subscriptions = new ArrayList<>(); if (serviceId == null || serviceId.isEmpty()) { subscriptions.addAll(accountDao.getActiveSubscriptions(accountId)); } else { final Subscription activeSubscription = accountDao.getActiveSubscription(accountId, serviceId); if (activeSubscription != null) { subscriptions.add(activeSubscription); } } final List<SubscriptionDescriptor> result = new ArrayList<>(subscriptions.size()); for (Subscription subscription : subscriptions) { result.add(toDescriptor(subscription, securityContext, null)); } return result; } /** * Returns {@link SubscriptionDescriptor} for subscription with given identifier. * * @param subscriptionId * subscription identifier * @return descriptor of subscription * @throws NotFoundException * when subscription with given identifier doesn't exist * @throws ForbiddenException * when user hasn't access to call this method * @see SubscriptionDescriptor * @see #getSubscriptions(String, String serviceId, SecurityContext) * @see #removeSubscription(String, SecurityContext) */ @ApiOperation(value = "Get subscription details", notes = "Get information on a particular subscription by its unique ID.", response = SubscriptionDescriptor.class, position = 11) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 403, message = "User not authorized to call this method"), @ApiResponse(code = 404, message = "Account ID not found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/subscriptions/{subscriptionId}") @RolesAllowed({"user", "system/admin", "system/manager"}) @Produces(MediaType.APPLICATION_JSON) public SubscriptionDescriptor getSubscriptionById(@ApiParam(value = "Subscription ID", required = true) @PathParam("subscriptionId") String subscriptionId, @Context SecurityContext securityContext) throws NotFoundException, ServerException, ForbiddenException { final Subscription subscription = accountDao.getSubscriptionById(subscriptionId); Set<String> roles = null; if (securityContext.isUserInRole("user")) { roles = resolveRolesForSpecificAccount(subscription.getAccountId()); if (!roles.contains("account/owner") && !roles.contains("account/member")) { throw new ForbiddenException("Access denied"); } } return toDescriptor(subscription, securityContext, roles); } /** * Validates addition of the subscription * * @param subscriptionTemplate * template of the subscription * @return {@link org.eclipse.che.api.account.shared.dto.NewSubscriptionTemplate} * @throws NotFoundException * if requested plan is not found * @throws ConflictException * if requested subscription can't be added * @throws ServerException */ @ApiOperation(value = "Validate new subscription", notes = "This method can be used prior to adding a new subscription to an account to make sure such a subscription can be added.", response = NewSubscriptionTemplate.class, position = 16) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 403, message = "Access denied"), @ApiResponse(code = 404, message = "Invalid subscription ID"), @ApiResponse(code = 409, message = "Plan and account identifier required"), @ApiResponse(code = 500, message = "Internal Server Error")}) @POST @Path("/subscriptions/validate") @RolesAllowed({"user", "system/admin", "system/manager"}) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public NewSubscriptionTemplate validateSubscriptionAddition(@ApiParam(value = "Subscription template", required = true) NewSubscriptionTemplate subscriptionTemplate, @Context SecurityContext securityContext) throws NotFoundException, ServerException, ConflictException, ForbiddenException { if (null == subscriptionTemplate || null == subscriptionTemplate.getAccountId() || null == subscriptionTemplate.getPlanId()) { throw new ConflictException("Plan and account identifier required"); } if (securityContext.isUserInRole("user") && !resolveRolesForSpecificAccount(subscriptionTemplate.getAccountId()).contains("account/owner")) { throw new ForbiddenException("Access denied"); } final Plan plan = planDao.getPlanById(subscriptionTemplate.getPlanId()); // allow regular user use subscription without trial or with trial which duration equal to duration from the plan if (subscriptionTemplate.getTrialDuration() != null && subscriptionTemplate.getTrialDuration() != 0 && !subscriptionTemplate.getTrialDuration().equals(plan.getTrialDuration()) && securityContext.isUserInRole("user")) { throw new ConflictException("Trial duration " + subscriptionTemplate.getTrialDuration() + " is not allowed"); } final SubscriptionService service = registry.get(plan.getServiceId()); //create new subscription final Subscription subscription = new Subscription().withAccountId(subscriptionTemplate.getAccountId()) .withServiceId(plan.getServiceId()) .withPlanId(plan.getId()) .withProperties(plan.getProperties()); service.beforeCreateSubscription(subscription); // check that user hasn't got trial before, omit for privileged user (e.g. system/admin) if (subscriptionTemplate.getTrialDuration() != null && subscriptionTemplate.getTrialDuration() != 0 && securityContext.isUserInRole("user")) { try { List<Subscription> subscriptions = accountDao.getSubscriptionQueryBuilder() .getTrialQuery(subscription.getServiceId(), subscription.getAccountId()) .execute(); if (!subscriptions.isEmpty()) { throw new ForbiddenException("Can't add new trial. Please, contact support"); } } catch (ServerException e) { throw new ServerException("Can't add subscription. Please, contact support"); } } return subscriptionTemplate; } /** * <p>Creates new subscription. Returns {@link SubscriptionDescriptor} * when subscription has been created successfully. * <p>Each new subscription should contain plan id and account id </p> * * @param newSubscription * new subscription * @return descriptor of created subscription * @throws ConflictException * when new subscription is {@code null} * or new subscription plan identifier is {@code null} * or new subscription account identifier is {@code null} * @throws NotFoundException * if plan with certain identifier is not found * @throws org.eclipse.che.api.core.ApiException * @see SubscriptionDescriptor * @see #getSubscriptionById(String, SecurityContext) * @see #removeSubscription(String, SecurityContext) */ @ApiOperation(value = "Add new subscription", notes = "Add a new subscription to an account. JSON with subscription details is sent. Roles: account/owner, system/admin.", response = SubscriptionDescriptor.class, position = 12) @ApiResponses(value = { @ApiResponse(code = 201, message = "CREATED"), @ApiResponse(code = 403, message = "Access denied"), @ApiResponse(code = 404, message = "Invalid subscription parameter"), @ApiResponse(code = 409, message = "Unknown ServiceID is used or payment token is invalid"), @ApiResponse(code = 500, message = "Internal Server Error")}) @POST @Path("/subscriptions") @GenerateLink(rel = Constants.LINK_REL_ADD_SUBSCRIPTION) @RolesAllowed({"user", "system/admin", "system/manager"}) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response addSubscription(@ApiParam(value = "Subscription details", required = true) @Required NewSubscription newSubscription, @Context SecurityContext securityContext) throws ApiException { requiredNotNull(newSubscription, "New subscription"); requiredNotNull(newSubscription.getAccountId(), "Account identifier"); requiredNotNull(newSubscription.getPlanId(), "Plan identifier"); requiredNotNull(newSubscription.getUsePaymentSystem(), "Use payment system"); //check user has access to add subscription final Set<String> roles = new HashSet<>(); if (securityContext.isUserInRole("user")) { roles.addAll(resolveRolesForSpecificAccount(newSubscription.getAccountId())); if (!roles.contains("account/owner")) { throw new ForbiddenException("Access denied"); } } final Plan plan = planDao.getPlanById(newSubscription.getPlanId()); // check service exists final SubscriptionService service = registry.get(plan.getServiceId()); if (null == service) { throw new ConflictException("Unknown serviceId is used"); } //Not admin has additional restrictions if (!securityContext.isUserInRole("system/admin") && !securityContext.isUserInRole("system/manager")) { // check that subscription is allowed for not admin if (plan.getSalesOnly()) { throw new ForbiddenException("User not authorized to add this subscription, please contact support"); } // only admins are allowed to disable payment on subscription addition if (!newSubscription.getUsePaymentSystem().equals(plan.isPaid())) { throw new ConflictException("Given value of attribute usePaymentSystem is not allowed"); } // check trial if (newSubscription.getTrialDuration() != null && newSubscription.getTrialDuration() != 0) { // allow regular user use subscription without trial or with trial which duration equal to duration from the plan if (!newSubscription.getTrialDuration().equals(plan.getTrialDuration())) { throw new ConflictException("User not authorized to add this subscription, please contact support"); } // check that user hasn't got trial before, omit for privileged user (e.g. system/admin) try { List<Subscription> subscriptions = accountDao.getSubscriptionQueryBuilder() .getTrialQuery(plan.getServiceId(), newSubscription.getAccountId()) .execute(); if (!subscriptions.isEmpty()) { throw new ForbiddenException("Can't add new trial. Please, contact support"); } } catch (ServerException e) { throw new ServerException("Can't add subscription. Please, contact support"); } } } // disable payment if subscription is free if (!plan.isPaid()) { newSubscription.setUsePaymentSystem(false); } //create new subscription Subscription subscription = new Subscription() .withId(NameGenerator.generate(Subscription.class.getSimpleName().toLowerCase(), Constants.ID_LENGTH)) .withAccountId(newSubscription.getAccountId()) .withUsePaymentSystem(newSubscription.getUsePaymentSystem()) .withServiceId(plan.getServiceId()) .withPlanId(plan.getId()) .withProperties(plan.getProperties()) .withDescription(plan.getDescription()) .withBillingCycleType(plan.getBillingCycleType()) .withBillingCycle(plan.getBillingCycle()) .withBillingContractTerm(plan.getBillingContractTerm()) .withState(SubscriptionState.ACTIVE); if (newSubscription.getTrialDuration() != null && newSubscription.getTrialDuration() != 0) { Calendar calendar = Calendar.getInstance(); subscription.setTrialStartDate(calendar.getTime()); calendar.add(Calendar.DATE, newSubscription.getTrialDuration()); subscription.setTrialEndDate(calendar.getTime()); } service.beforeCreateSubscription(subscription); LOG.info("Add subscription# id#{}# userId#{}# accountId#{}# planId#{}#", subscription.getId(), EnvironmentContext.getCurrent().getUser().getId(), subscription.getAccountId(), subscription.getPlanId()); accountDao.addSubscription(subscription); service.afterCreateSubscription(subscription); LOG.info("Added subscription. Subscription ID #{}# Account ID #{}#", subscription.getId(), subscription.getAccountId()); return Response.status(Response.Status.CREATED) .entity(toDescriptor(subscription, securityContext, roles)) .build(); } /** * Removes subscription by id. Actually makes it inactive. * * @param subscriptionId * id of the subscription to remove * @throws NotFoundException * if subscription with such id is not found * @throws ForbiddenException * if user hasn't permissions * @throws ServerException * if internal server error occurs * @throws org.eclipse.che.api.core.ApiException * @see #addSubscription(NewSubscription, SecurityContext) * @see #getSubscriptions(String, String, SecurityContext) */ @ApiOperation(value = "Remove subscription", notes = "Remove subscription from account. Roles: account/owner, system/admin.", position = 13) @ApiResponses(value = { @ApiResponse(code = 204, message = "OK"), @ApiResponse(code = 403, message = "Access denied"), @ApiResponse(code = 404, message = "Invalid subscription ID"), @ApiResponse(code = 500, message = "Internal Server Error")}) @DELETE @Path("/subscriptions/{subscriptionId}") @RolesAllowed({"user", "system/admin", "system/manager"}) public void removeSubscription(@ApiParam(value = "Subscription ID", required = true) @PathParam("subscriptionId") String subscriptionId, @Context SecurityContext securityContext) throws ApiException { final Subscription toRemove = accountDao.getSubscriptionById(subscriptionId); if (securityContext.isUserInRole("user") && !resolveRolesForSpecificAccount(toRemove.getAccountId()).contains("account/owner")) { throw new ForbiddenException("Access denied"); } if (SubscriptionState.INACTIVE == toRemove.getState()) { throw new ForbiddenException("Subscription is inactive already " + subscriptionId); } LOG.info("Remove subscription# id#{}# userId#{}# accountId#{}#", subscriptionId, EnvironmentContext.getCurrent().getUser().getId(), toRemove.getAccountId()); toRemove.setState(SubscriptionState.INACTIVE); accountDao.updateSubscription(toRemove); final SubscriptionService service = registry.get(toRemove.getServiceId()); service.onRemoveSubscription(toRemove); } @ApiOperation(value = "Remove account", notes = "Remove subscription from account. JSON with subscription details is sent. Can be performed only by system/admin.", position = 16) @ApiResponses(value = { @ApiResponse(code = 204, message = "OK"), @ApiResponse(code = 403, message = "Access denied"), @ApiResponse(code = 404, message = "Invalid account ID"), @ApiResponse(code = 409, message = "Cannot delete account with associated workspaces"), @ApiResponse(code = 500, message = "Internal Server Error")}) @DELETE @Path("/{id}") @RolesAllowed("system/admin") public void remove(@ApiParam(value = "Account ID", required = true) @PathParam("id") String id) throws NotFoundException, ServerException, ConflictException { accountDao.remove(id); } /** * Redistributes resources between workspaces * * @param id * account id * @param updateResourcesDescriptors * descriptor of resources for updating * @throws ForbiddenException * when account hasn't permission for setting attribute in workspace * @throws NotFoundException * when account or workspace with given id doesn't exist * @throws ConflictException * when account hasn't required Saas subscription * or user want to use more RAM than he has * @throws ServerException */ @ApiOperation(value = "Redistributes resources", notes = "Redistributes resources between workspaces. Roles: account/owner, system/manager, system/admin.", position = 17) @ApiResponses(value = { @ApiResponse(code = 204, message = "OK"), @ApiResponse(code = 403, message = "Access denied"), @ApiResponse(code = 404, message = "Not found"), @ApiResponse(code = 409, message = "Conflict Error"), @ApiResponse(code = 500, message = "Internal Server Error")}) @POST @Path("/{id}/resources") @RolesAllowed({"account/owner", "system/manager", "system/admin"}) @Consumes(MediaType.APPLICATION_JSON) public void redistributeResources(@ApiParam(value = "Account ID", required = true) @PathParam("id") String id, @ApiParam(value = "Resources description", required = true) @Required List<UpdateResourcesDescriptor> updateResourcesDescriptors) throws ForbiddenException, ConflictException, NotFoundException, ServerException { resourcesManager.redistributeResources(id, updateResourcesDescriptors); } /** * Returns used resources, provided by subscriptions * * @param accountId * account id */ @ApiOperation(value = "Get used resources, provided by subscriptions", notes = "Returns used resources, provided by subscriptions. Roles: account/owner, account/member, system/manager, system/admin.", position = 17) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Not found"), @ApiResponse(code = 500, message = "Internal Server Error")}) @GET @Path("/{id}/resources") @RolesAllowed({"account/owner", "account/member", "system/manager", "system/admin"}) @Produces(MediaType.APPLICATION_JSON) public List<SubscriptionResourcesUsed> getResources(@ApiParam(value = "Account ID", required = true) @PathParam("id") String accountId, @QueryParam("serviceId") String serviceId) throws ServerException, NotFoundException, ConflictException { Set<SubscriptionService> subscriptionServices = new HashSet<>(); if (serviceId == null) { subscriptionServices.addAll(registry.getAll()); } else { final SubscriptionService subscriptionService = registry.get(serviceId); if (subscriptionService == null) { throw new ConflictException("Unknown serviceId is used"); } subscriptionServices.add(subscriptionService); } List<SubscriptionResourcesUsed> result = new ArrayList<>(); for (SubscriptionService subscriptionService : subscriptionServices) { Subscription activeSubscription = accountDao.getActiveSubscription(accountId, subscriptionService.getServiceId()); if (activeSubscription != null) { //For now account can have only one subscription for each service UsedAccountResources usedAccountResources = subscriptionService.getAccountResources(activeSubscription); result.add(DtoFactory.getInstance().createDto(SubscriptionResourcesUsed.class) .withUsed(usedAccountResources.getUsed()) .withSubscriptionReference(toReference(activeSubscription))); } } return result; } /** * Can be used only in methods that is restricted with @RolesAllowed. Require "user" role. * * @param currentAccountId * account id to resolve roles for * @return set of user roles */ private Set<String> resolveRolesForSpecificAccount(String currentAccountId) { try { final String userId = EnvironmentContext.getCurrent().getUser().getId(); for (Member membership : accountDao.getByMember(userId)) { if (membership.getAccountId().equals(currentAccountId)) { return new HashSet<>(membership.getRoles()); } } } catch (ApiException ignored) { } return Collections.emptySet(); } private void validateAttributeName(String attributeName) throws ConflictException { if (attributeName == null || attributeName.isEmpty() || attributeName.toLowerCase().startsWith("codenvy")) { throw new ConflictException(format("Attribute name '%s' is not valid", attributeName)); } } /** Converts {@link Account} to {@link AccountDescriptor} */ private AccountDescriptor toDescriptor(Account account, SecurityContext securityContext) { final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder(); final List<Link> links = new LinkedList<>(); links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getMemberships") .build() .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_ACCOUNTS)); links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getSubscriptions") .build(account.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_SUBSCRIPTIONS)); links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getMembers") .build(account.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_MEMBERS)); links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getById") .build(account.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_ACCOUNT_BY_ID)); links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getResources") .build(account.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_ACCOUNT_RESOURCES)); if (securityContext.isUserInRole("system/admin") || securityContext.isUserInRole("system/manager")) { links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getByName") .queryParam("name", account.getName()) .build() .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_ACCOUNT_BY_NAME)); } if (securityContext.isUserInRole("system/admin")) { links.add(LinksHelper.createLink(HttpMethod.DELETE, uriBuilder.clone().path(getClass(), "remove") .build(account.getId()) .toString(), null, null, Constants.LINK_REL_REMOVE_ACCOUNT)); } if (!securityContext.isUserInRole("account/owner") && !securityContext.isUserInRole("account/member") && !securityContext.isUserInRole("system/admin") && !securityContext.isUserInRole("system/manager")) { account.getAttributes().clear(); } account.getAttributes().remove("codenvy:creditCardToken"); account.getAttributes().remove("codenvy:billing.date"); return DtoFactory.getInstance().createDto(AccountDescriptor.class) .withId(account.getId()) .withName(account.getName()) .withAttributes(account.getAttributes()) .withLinks(links); } /** * Converts {@link Member} to {@link MemberDescriptor} */ private MemberDescriptor toDescriptor(Member member, Account account, SecurityContext securityContext) { final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder(); final Link removeMember = LinksHelper.createLink(HttpMethod.DELETE, uriBuilder.clone() .path(getClass(), "removeMember") .build(account.getId(), member.getUserId()) .toString(), null, null, Constants.LINK_REL_REMOVE_MEMBER); final Link allMembers = LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getMembers") .build(account.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_MEMBERS); final AccountReference accountRef = DtoFactory.getInstance().createDto(AccountReference.class) .withId(account.getId()) .withName(account.getName()); if (member.getRoles().contains("account/owner") || securityContext.isUserInRole("system/admin") || securityContext.isUserInRole("system/manager")) { accountRef.setLinks(singletonList(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getById") .build(account.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_ACCOUNT_BY_ID))); } return DtoFactory.getInstance().createDto(MemberDescriptor.class) .withUserId(member.getUserId()) .withRoles(member.getRoles()) .withAccountReference(accountRef) .withLinks(Arrays.asList(removeMember, allMembers)); } /** * Checks object reference is not {@code null} * * @param object * object reference to check * @param subject * used as subject of exception message "{subject} required" * @throws ConflictException * when object reference is {@code null} */ private void requiredNotNull(Object object, String subject) throws ConflictException { if (object == null) { throw new ConflictException(subject + " required"); } } /** * Create {@link SubscriptionDescriptor} from {@link Subscription}. * Set with roles should be used if account roles can't be resolved with {@link SecurityContext} * (If there is no id of the account in the REST path.) * * @param subscription * subscription that should be converted to {@link SubscriptionDescriptor} * @param resolvedRoles * resolved roles. Do not use if id of the account presents in REST path. */ private SubscriptionDescriptor toDescriptor(Subscription subscription, SecurityContext securityContext, Set resolvedRoles) { List<Link> links = new ArrayList<>(0); // community subscriptions should not use urls if (!"sas-community".equals(subscription.getPlanId())) { final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder(); links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getSubscriptionById") .build(subscription.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_SUBSCRIPTION)); boolean isUserPrivileged = (resolvedRoles != null && resolvedRoles.contains("account/owner")) || securityContext.isUserInRole("account/owner") || securityContext.isUserInRole("system/admin") || securityContext.isUserInRole("system/manager"); if (SubscriptionState.ACTIVE.equals(subscription.getState()) && isUserPrivileged) { links.add(LinksHelper.createLink(HttpMethod.DELETE, uriBuilder.clone() .path(getClass(), "removeSubscription") .build(subscription.getId()) .toString(), null, null, Constants.LINK_REL_REMOVE_SUBSCRIPTION)); } } // Do not send with REST properties that starts from 'codenvy:' LinkedHashMap<String, String> filteredProperties = new LinkedHashMap<>(); for (Map.Entry<String, String> property : subscription.getProperties().entrySet()) { if (!property.getKey().startsWith("codenvy:") || securityContext.isUserInRole("system/admin") || securityContext.isUserInRole("system/manager")) { filteredProperties.put(property.getKey(), property.getValue()); } } SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); dateFormat.setLenient(false); return DtoFactory.getInstance().createDto(SubscriptionDescriptor.class) .withId(subscription.getId()) .withAccountId(subscription.getAccountId()) .withServiceId(subscription.getServiceId()) .withProperties(filteredProperties) .withPlanId(subscription.getPlanId()) .withState(subscription.getState()) .withDescription(subscription.getDescription()) .withStartDate(null == subscription.getStartDate() ? null : dateFormat.format(subscription.getStartDate())) .withEndDate(null == subscription.getEndDate() ? null : dateFormat.format(subscription.getEndDate())) .withTrialStartDate( null == subscription.getTrialStartDate() ? null : dateFormat.format(subscription.getTrialStartDate())) .withTrialEndDate( null == subscription.getTrialEndDate() ? null : dateFormat.format(subscription.getTrialEndDate())) .withUsePaymentSystem(subscription.getUsePaymentSystem()) .withBillingStartDate( null == subscription.getBillingStartDate() ? null : dateFormat.format(subscription.getBillingStartDate())) .withBillingEndDate( null == subscription.getBillingEndDate() ? null : dateFormat.format(subscription.getBillingEndDate())) .withNextBillingDate( null == subscription.getNextBillingDate() ? null : dateFormat.format(subscription.getNextBillingDate())) .withBillingCycle(subscription.getBillingCycle()) .withBillingCycleType(subscription.getBillingCycleType()) .withBillingContractTerm(subscription.getBillingContractTerm()) .withLinks(links); } /** * Create {@link SubscriptionReference} from {@link Subscription}. * * @param subscription * subscription that should be converted to {@link SubscriptionReference} */ private SubscriptionReference toReference(Subscription subscription) { List<Link> links = new ArrayList<>(0); // community subscriptions should not use urls if (!"sas-community".equals(subscription.getPlanId())) { final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder(); links.add(LinksHelper.createLink(HttpMethod.GET, uriBuilder.clone() .path(getClass(), "getSubscriptionById") .build(subscription.getId()) .toString(), null, MediaType.APPLICATION_JSON, Constants.LINK_REL_GET_SUBSCRIPTION)); } return DtoFactory.getInstance().createDto(SubscriptionReference.class) .withSubscriptionId(subscription.getId()) .withServiceId(subscription.getServiceId()) .withDescription(subscription.getDescription()) .withPlanId(subscription.getPlanId()) .withLinks(links); } // TODO remove it after testing! @GET @Path("/subscriptions/test") @RolesAllowed({"user"}) public void test() { LOG.info("Subscription scheduler test is started"); try { for (SubscriptionService subscriptionService : registry.getAll()) { subscriptionService.onCheckSubscriptions(); } } catch (Exception e) { LOG.error(e.getLocalizedMessage(), e); } LOG.info("Subscription scheduler test is finished"); } }