com.zimbra.cs.service.ExternalUserProvServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.service.ExternalUserProvServlet.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software Foundation,
 * version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with this program.
 * If not, see <https://www.gnu.org/licenses/>.
 * ***** END LICENSE BLOCK *****
 */

package com.zimbra.cs.service;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Hex;

import com.google.common.collect.Lists;
import com.zimbra.client.ZFolder;
import com.zimbra.client.ZMailbox;
import com.zimbra.client.ZMountpoint;
import com.zimbra.common.account.ProvisioningConstants;
import com.zimbra.common.localconfig.DebugConfig;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.util.BlobMetaData;
import com.zimbra.common.util.L10nUtil;
import com.zimbra.common.util.Log;
import com.zimbra.common.util.LogFactory;
import com.zimbra.common.util.StringUtil;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.AuthToken;
import com.zimbra.cs.account.AuthTokenException;
import com.zimbra.cs.account.Domain;
import com.zimbra.cs.account.ExtAuthTokenKey;
import com.zimbra.cs.account.GuestAccount;
import com.zimbra.cs.account.NamedEntry;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.account.SearchAccountsOptions;
import com.zimbra.cs.account.ShareInfoData;
import com.zimbra.cs.account.TokenUtil;
import com.zimbra.cs.ldap.ZLdapFilterFactory;
import com.zimbra.cs.mailbox.ACL;
import com.zimbra.cs.mailbox.Flag;
import com.zimbra.cs.mailbox.MailItem;
import com.zimbra.cs.mailbox.Mailbox;
import com.zimbra.cs.mailbox.MailboxManager;
import com.zimbra.cs.mailbox.Mountpoint;
import com.zimbra.cs.mailbox.acl.AclPushSerializer;
import com.zimbra.cs.servlet.ZimbraServlet;
import com.zimbra.cs.util.AccountUtil;
import com.zimbra.cs.util.WebClientServiceUtil;
import com.zimbra.soap.mail.message.FolderActionRequest;
import com.zimbra.soap.mail.type.FolderActionSelector;

public class ExternalUserProvServlet extends ZimbraServlet {

    private static final Log logger = LogFactory.getLog(ExternalUserProvServlet.class);
    private static final String EXT_USER_PROV_ON_UI_NODE = "/fromservice/extuserprov";
    private static final String PUBLIC_LOGIN_ON_UI_NODE = "/fromservice/publiclogin";
    public static final String PUBLIC_EXTUSERPROV_JSP = "/public/extuserprov.jsp";
    public static final String PUBLIC_LOGIN_JSP = "/public/login.jsp";

    @Override
    public void init() throws ServletException {
        String name = getServletName();
        ZimbraLog.account.info("Servlet " + name + " starting up");
        super.init();
    }

    @Override
    public void destroy() {
        String name = getServletName();
        ZimbraLog.account.info("Servlet " + name + " shutting down");
        super.destroy();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String param = req.getParameter("p");
        if (param == null) {
            throw new ServletException("request missing param");
        }
        Map<Object, Object> tokenMap = validatePrelimToken(param);
        Map<String, String> reqHeaders = new HashMap<String, String>();
        String ownerId = (String) tokenMap.get("aid");
        String folderId = (String) tokenMap.get("fid");
        String extUserEmail = (String) tokenMap.get("email");

        Provisioning prov = Provisioning.getInstance();
        Account grantee;
        try {
            Account owner = prov.getAccountById(ownerId);
            Domain domain = prov.getDomain(owner);
            grantee = prov.getAccountByName(mapExtEmailToAcctName(extUserEmail, domain));
            if (grantee == null) {
                // external virtual account not created yet
                if (prov.isOctopus() && DebugConfig.skipVirtualAccountRegistrationPage) {
                    // provision using 'null' password and display name
                    // UI will ask the user to set these post provisioning
                    provisionVirtualAccountAndRedirect(req, resp, null, null, ownerId, extUserEmail);
                } else {
                    resp.addCookie(new Cookie("ZM_PRELIM_AUTH_TOKEN", param));
                    req.setAttribute("extuseremail", extUserEmail);
                    if (WebClientServiceUtil.isServerInSplitMode()) {
                        reqHeaders.put("extuseremail", extUserEmail);
                        reqHeaders.put("ZM_PRELIM_AUTH_TOKEN", param);
                        String htmlresp = WebClientServiceUtil
                                .sendServiceRequestToOneRandomUiNode(EXT_USER_PROV_ON_UI_NODE, reqHeaders);
                        resp.getWriter().print(htmlresp);
                    } else {
                        ServletContext context = getServletContext().getContext("/zimbra");
                        if (context != null) {
                            RequestDispatcher dispatcher = context.getRequestDispatcher(PUBLIC_EXTUSERPROV_JSP);
                            dispatcher.forward(req, resp);
                        } else {
                            logger.warn("Could not access servlet context url /zimbra");
                            throw ServiceException.TEMPORARILY_UNAVAILABLE();
                        }
                    }
                }
            } else {
                // create a new mountpoint in the external user's mailbox if not already created

                String[] sharedItems = owner.getSharedItem();
                int sharedFolderId = Integer.valueOf(folderId);
                String sharedFolderPath = null;
                MailItem.Type sharedFolderView = null;
                for (String sharedItem : sharedItems) {
                    ShareInfoData sid = AclPushSerializer.deserialize(sharedItem);
                    if (sid.getItemId() == sharedFolderId && extUserEmail.equalsIgnoreCase(sid.getGranteeId())) {
                        sharedFolderPath = sid.getPath();
                        sharedFolderView = sid.getFolderDefaultViewCode();
                        break;
                    }
                }
                if (sharedFolderPath == null) {
                    throw new ServletException("share not found");
                }
                String mountpointName = getMountpointName(owner, grantee, sharedFolderPath);

                ZMailbox.Options options = new ZMailbox.Options();
                options.setNoSession(true);
                options.setAuthToken(AuthProvider.getAuthToken(grantee).toZAuthToken());
                options.setUri(AccountUtil.getSoapUri(grantee));
                ZMailbox zMailbox = new ZMailbox(options);
                ZMountpoint zMtpt = null;
                try {
                    zMtpt = zMailbox.createMountpoint(String.valueOf(getMptParentFolderId(sharedFolderView, prov)),
                            mountpointName, ZFolder.View.fromString(sharedFolderView.toString()),
                            ZFolder.Color.DEFAULTCOLOR, null, ZMailbox.OwnerBy.BY_ID, ownerId,
                            ZMailbox.SharedItemBy.BY_ID, folderId, false);
                } catch (ServiceException e) {
                    logger.debug("Error in attempting to create mountpoint. Probably it already exists.", e);
                }
                if (zMtpt != null) {
                    if (sharedFolderView == MailItem.Type.APPOINTMENT) {
                        // make sure that the mountpoint is checked in the UI by default
                        FolderActionSelector actionSelector = new FolderActionSelector(zMtpt.getId(), "check");
                        FolderActionRequest actionRequest = new FolderActionRequest(actionSelector);
                        try {
                            zMailbox.invokeJaxb(actionRequest);
                        } catch (ServiceException e) {
                            logger.warn("Error in invoking check action on calendar mountpoint", e);
                        }
                    }
                    HashSet<MailItem.Type> types = new HashSet<MailItem.Type>();
                    types.add(sharedFolderView);
                    enableAppFeatures(grantee, types);
                }

                // check if the external user is already logged-in
                String zAuthTokenCookie = null;
                javax.servlet.http.Cookie cookies[] = req.getCookies();
                if (cookies != null) {
                    for (Cookie cookie : cookies) {
                        if (cookie.getName().equals("ZM_AUTH_TOKEN")) {
                            zAuthTokenCookie = cookie.getValue();
                            break;
                        }
                    }
                }
                AuthToken zAuthToken = null;
                if (zAuthTokenCookie != null) {
                    try {
                        zAuthToken = AuthProvider.getAuthToken(zAuthTokenCookie);
                    } catch (AuthTokenException ignored) {
                        // auth token is not valid
                    }
                }
                if (zAuthToken != null && !zAuthToken.isExpired() && zAuthToken.isRegistered()
                        && grantee.getId().equals(zAuthToken.getAccountId())) {
                    // external virtual account already logged-in
                    resp.sendRedirect("/");
                } else if (prov.isOctopus() && !grantee.isVirtualAccountInitialPasswordSet()
                        && DebugConfig.skipVirtualAccountRegistrationPage) {
                    // seems like the virtual user did not set his password during his last visit, after an account was
                    // provisioned for him
                    setCookieAndRedirect(req, resp, grantee);
                } else {
                    req.setAttribute("virtualacctdomain", domain.getName());
                    if (WebClientServiceUtil.isServerInSplitMode()) {
                        reqHeaders.put("virtualacctdomain", domain.getName());
                        String htmlresp = WebClientServiceUtil
                                .sendServiceRequestToOneRandomUiNode(PUBLIC_LOGIN_ON_UI_NODE, reqHeaders);
                        resp.getWriter().print(htmlresp);
                    } else {
                        RequestDispatcher dispatcher = getServletContext().getContext("/zimbra")
                                .getRequestDispatcher(PUBLIC_LOGIN_JSP);
                        dispatcher.forward(req, resp);
                    }
                }
            }
        } catch (ServiceException e) {
            throw new ServletException(e);
        }
    }

    private static String getMountpointName(Account owner, Account grantee, String sharedFolderPath)
            throws ServiceException {
        if (sharedFolderPath.startsWith("/")) {
            sharedFolderPath = sharedFolderPath.substring(1);
        }
        int index = sharedFolderPath.indexOf('/');
        if (index != -1) {
            // exclude the top level folder name, such as "Briefcase"
            sharedFolderPath = sharedFolderPath.substring(index + 1);
        }
        return L10nUtil.getMessage(L10nUtil.MsgKey.shareNameDefault, grantee.getLocale(), getDisplayName(owner),
                sharedFolderPath.replace("/", " "));
    }

    private static String getDisplayName(Account owner) {
        return owner.getDisplayName() != null ? owner.getDisplayName() : owner.getName();
    }

    private static String mapExtEmailToAcctName(String extUserEmail, Domain domain) {
        return extUserEmail.replace("@", ".") + "@" + domain.getName();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String displayName = req.getParameter("displayname");
        String password = req.getParameter("password");

        String prelimToken = null;
        javax.servlet.http.Cookie cookies[] = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("ZM_PRELIM_AUTH_TOKEN")) {
                    prelimToken = cookie.getValue();
                    break;
                }
            }
        }
        if (prelimToken == null) {
            throw new ServletException("unauthorized request");
        }
        Map<Object, Object> tokenMap = validatePrelimToken(prelimToken);
        String ownerId = (String) tokenMap.get("aid");
        //        String folderId = (String) tokenMap.get("fid");
        String extUserEmail = (String) tokenMap.get("email");

        provisionVirtualAccountAndRedirect(req, resp, displayName, password, ownerId, extUserEmail);
    }

    private static void provisionVirtualAccountAndRedirect(HttpServletRequest req, HttpServletResponse resp,
            String displayName, String password, String grantorId, String extUserEmail) throws ServletException {
        Provisioning prov = Provisioning.getInstance();
        try {
            Account owner = prov.getAccountById(grantorId);
            Domain domain = prov.getDomain(owner);
            Account grantee = prov.getAccountByName(mapExtEmailToAcctName(extUserEmail, domain));
            if (grantee != null) {
                throw new ServletException("invalid request: account already exists");
            }

            // search all shares accessible to the external user
            SearchAccountsOptions searchOpts = new SearchAccountsOptions(domain, new String[] {
                    Provisioning.A_zimbraId, Provisioning.A_displayName, Provisioning.A_zimbraSharedItem });
            // get all groups extUserEmail belongs to
            GuestAccount guestAcct = new GuestAccount(extUserEmail, null);
            List<String> groupIds = prov.getGroupMembership(guestAcct, false).groupIds();
            List<String> grantees = Lists.newArrayList(extUserEmail);
            grantees.addAll(groupIds);
            searchOpts.setFilter(ZLdapFilterFactory.getInstance().accountsByGrants(grantees, false, false));
            List<NamedEntry> accounts = prov.searchDirectory(searchOpts);

            if (accounts.isEmpty()) {
                throw new ServletException("no shares discovered");
            }

            // create external account
            Map<String, Object> attrs = new HashMap<String, Object>();
            attrs.put(Provisioning.A_zimbraIsExternalVirtualAccount, ProvisioningConstants.TRUE);
            attrs.put(Provisioning.A_zimbraExternalUserMailAddress, extUserEmail);
            attrs.put(Provisioning.A_zimbraMailHost, prov.getLocalServer().getServiceHostname());
            if (!StringUtil.isNullOrEmpty(displayName)) {
                attrs.put(Provisioning.A_displayName, displayName);
            }
            attrs.put(Provisioning.A_zimbraHideInGal, ProvisioningConstants.TRUE);
            attrs.put(Provisioning.A_zimbraMailStatus, Provisioning.MailStatus.disabled.toString());
            if (!StringUtil.isNullOrEmpty(password)) {
                attrs.put(Provisioning.A_zimbraVirtualAccountInitialPasswordSet, ProvisioningConstants.TRUE);
            }
            grantee = prov.createAccount(mapExtEmailToAcctName(extUserEmail, domain), password, attrs);

            // create external account mailbox
            Mailbox granteeMbox;
            try {
                granteeMbox = MailboxManager.getInstance().getMailboxByAccount(grantee);
            } catch (ServiceException e) {
                // mailbox creation failed; delete the account also so that it is a clean state before
                // the next attempt
                prov.deleteAccount(grantee.getId());
                throw e;
            }

            // create mountpoints
            Set<MailItem.Type> viewTypes = new HashSet<MailItem.Type>();
            for (NamedEntry ne : accounts) {
                Account account = (Account) ne;
                String[] sharedItems = account.getSharedItem();
                for (String sharedItem : sharedItems) {
                    ShareInfoData shareData = AclPushSerializer.deserialize(sharedItem);
                    if (!granteeMatchesShare(shareData, grantee)) {
                        continue;
                    }
                    String sharedFolderPath = shareData.getPath();
                    String mountpointName = getMountpointName(account, grantee, sharedFolderPath);
                    MailItem.Type viewType = shareData.getFolderDefaultViewCode();
                    Mountpoint mtpt = granteeMbox.createMountpoint(null, getMptParentFolderId(viewType, prov),
                            mountpointName, account.getId(), shareData.getItemId(), shareData.getItemUuid(),
                            viewType, 0, MailItem.DEFAULT_COLOR, false);
                    if (viewType == MailItem.Type.APPOINTMENT) {
                        // make sure that the mountpoint is checked in the UI by default
                        granteeMbox.alterTag(null, mtpt.getId(), mtpt.getType(), Flag.FlagInfo.CHECKED, true, null);
                    }
                    viewTypes.add(viewType);
                }
            }
            enableAppFeatures(grantee, viewTypes);

            setCookieAndRedirect(req, resp, grantee);
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }

    private static boolean granteeMatchesShare(ShareInfoData shareData, Account acct) throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        String grantee = shareData.getGranteeId();
        byte granteeType = shareData.getGranteeTypeCode();
        switch (granteeType) {
        case ACL.GRANTEE_GROUP:
            return prov.inACLGroup(acct, grantee);
        case ACL.GRANTEE_GUEST:
            return grantee.equalsIgnoreCase(acct.getExternalUserMailAddress());
        default:
            return false;
        }
    }

    private static void setCookieAndRedirect(HttpServletRequest req, HttpServletResponse resp, Account grantee)
            throws ServiceException, IOException {
        AuthToken authToken = AuthProvider.getAuthToken(grantee);
        authToken.encode(resp, false, req.getScheme().equals("https"));
        resp.sendRedirect("/");
    }

    private static int getMptParentFolderId(MailItem.Type viewType, Provisioning prov) throws ServiceException {
        switch (viewType) {
        case DOCUMENT:
            if (prov.isOctopus()) {
                return Mailbox.ID_FOLDER_BRIEFCASE;
            }
        default:
            return Mailbox.ID_FOLDER_USER_ROOT;
        }
    }

    private static void enableAppFeatures(Account grantee, Set<MailItem.Type> viewTypes) throws ServiceException {
        Map<String, Object> appFeatureAttrs = new HashMap<String, Object>();
        for (MailItem.Type type : viewTypes) {
            switch (type) {
            case DOCUMENT:
                appFeatureAttrs.put(Provisioning.A_zimbraFeatureBriefcasesEnabled, ProvisioningConstants.TRUE);
                break;
            case APPOINTMENT:
                appFeatureAttrs.put(Provisioning.A_zimbraFeatureCalendarEnabled, ProvisioningConstants.TRUE);
                break;
            case CONTACT:
                appFeatureAttrs.put(Provisioning.A_zimbraFeatureContactsEnabled, ProvisioningConstants.TRUE);
                break;
            case TASK:
                appFeatureAttrs.put(Provisioning.A_zimbraFeatureTasksEnabled, ProvisioningConstants.TRUE);
                break;
            case MESSAGE:
                appFeatureAttrs.put(Provisioning.A_zimbraFeatureMailEnabled, ProvisioningConstants.TRUE);
                break;
            default:
                // we don't care about other types
            }
        }
        grantee.modify(appFeatureAttrs);
    }

    public static Map<Object, Object> validatePrelimToken(String param) throws ServletException {
        int pos = param.indexOf('_');
        if (pos == -1) {
            throw new ServletException("invalid token param");
        }
        String ver = param.substring(0, pos);
        int pos2 = param.indexOf('_', pos + 1);
        if (pos2 == -1) {
            throw new ServletException("invalid token param");
        }
        String hmac = param.substring(pos + 1, pos2);
        String data = param.substring(pos2 + 1);
        Map<Object, Object> map;
        try {
            ExtAuthTokenKey key = ExtAuthTokenKey.getVersion(ver);
            if (key == null) {
                throw new ServletException("unknown key version");
            }
            String computedHmac = TokenUtil.getHmac(data, key.getKey());
            if (!computedHmac.equals(hmac)) {
                throw new ServletException("hmac failure");
            }
            String decoded = new String(Hex.decodeHex(data.toCharArray()));
            map = BlobMetaData.decode(decoded);
        } catch (Exception e) {
            throw new ServletException(e);
        }
        Object expiry = map.get("exp");
        if (expiry != null) {
            // check validity
            if (System.currentTimeMillis() > Long.parseLong((String) expiry)) {
                throw new ServletException("url no longer valid");
            }
        }
        return map;
    }
}