org.georchestra.console.ws.backoffice.users.UsersController.java Source code

Java tutorial

Introduction

Here is the source code for org.georchestra.console.ws.backoffice.users.UsersController.java

Source

/*
 * Copyright (C) 2009-2018 by the geOrchestra PSC
 *
 * This file is part of geOrchestra.
 *
 * geOrchestra 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, either version 3 of the License, or (at your option)
 * any later version.
 *
 * geOrchestra 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
 * geOrchestra.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.georchestra.console.ws.backoffice.users;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.georchestra.console.dao.AdvancedDelegationDao;
import org.georchestra.console.dao.DelegationDao;
import org.georchestra.console.ds.AccountDao;
import org.georchestra.console.ds.DataServiceException;
import org.georchestra.console.ds.DuplicatedEmailException;
import org.georchestra.console.ds.DuplicatedUidException;
import org.georchestra.console.ds.OrgsDao;
import org.georchestra.console.ds.ProtectedUserFilter;
import org.georchestra.console.ds.RoleDao;
import org.georchestra.console.dto.*;
import org.georchestra.console.mailservice.MailService;
import org.georchestra.console.model.DelegationEntry;
import org.georchestra.console.ws.backoffice.utils.RequestUtil;
import org.georchestra.console.ws.backoffice.utils.ResponseUtil;
import org.georchestra.console.ws.utils.Validation;
import org.georchestra.lib.file.FileUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.Normalizer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Web Services to maintain the User information.
 *
 * <p>
 * This class provides the operations to access the data layer to update and read the user data.
 * Those operations will be consistent with the business rules.
 * </p>
 *
 * @author Mauricio Pazos
 *
 */
@Controller
public class UsersController {

    private static final Log LOG = LogFactory.getLog(UsersController.class.getName());

    private static final String BASE_MAPPING = "/private";
    private static final String REQUEST_MAPPING = BASE_MAPPING + "/users";
    private static final String PUBLIC_REQUEST_MAPPING = "/public/users";

    private static final String DUPLICATED_EMAIL = "duplicated_email";
    private static final String PARAMS_NOT_UNDERSTOOD = "params_not_understood";
    private static final String NOT_FOUND = "not_found";
    private static final String UNABLE_TO_ENCODE = "unable_to_encode";
    private static final String INVALID_VALUE = "invalid_value";
    private static final String OTHER_ERROR = "other_error";
    private static final String INVALID_DATE_FORMAT = "invalid_date_format";

    private static GrantedAuthority ROLE_SUPERUSER = new SimpleGrantedAuthority("ROLE_SUPERUSER");

    private AccountDao accountDao;

    @Autowired
    private OrgsDao orgDao;

    @Autowired
    private RoleDao roleDao;

    @Autowired
    private DelegationDao delegationDao;

    @Autowired
    private AdvancedDelegationDao advancedDelegationDao;

    @Autowired
    private Validation validation;

    public void setOrgDao(OrgsDao orgDao) {
        this.orgDao = orgDao;
    }

    public void setDelegationDao(DelegationDao delegationDao) {
        this.delegationDao = delegationDao;
    }

    private UserRule userRule;

    @Autowired
    private MailService mailService;

    public void setMailService(MailService mailService) {
        this.mailService = mailService;
    }

    @Autowired
    private Boolean warnUserIfUidModified = false;

    public void setWarnUserIfUidModified(boolean warnUserIfUidModified) {
        this.warnUserIfUidModified = warnUserIfUidModified;
    }

    @Autowired
    public UsersController(AccountDao dao, UserRule userRule) {
        this.accountDao = dao;
        this.userRule = userRule;
    }

    /**
     * Returns array of users using json syntax.
     * <pre>
     *
     *   [
     *       {
     *           "org": "Zogak",
     *           "givenName": "Walsh",
     *           "sn": "Atkins",
     *           "uid": "watkins"
     *       },
     *           ...
     *   ]
     * </pre>
     *
     * @throws IOException
     */
    @RequestMapping(value = REQUEST_MAPPING, method = RequestMethod.GET, produces = "application/json; charset=utf-8")
    @ResponseBody
    @PostFilter("hasPermission(filterObject, 'read')")
    public List<SimpleAccount> findAll() throws DataServiceException {

        ProtectedUserFilter filter = new ProtectedUserFilter(this.userRule.getListUidProtected());
        List<Account> list = this.accountDao.findFilterBy(filter);
        Collections.sort(list);

        // Retrieve organizations list to display org name instead of org DN
        List<Org> orgs = this.orgDao.findAll();
        Map<String, String> orgNames = new HashMap<String, String>();
        for (Org org : orgs)
            orgNames.put(org.getId(), org.getName());

        List<SimpleAccount> res = new LinkedList<SimpleAccount>();
        for (Account account : list) {
            SimpleAccount simpleAccount = new SimpleAccount(account);
            // Set Org Name with the human readable org name
            simpleAccount.setOrgName(orgNames.get(account.getOrg()));
            res.add(simpleAccount);
        }

        return res;
    }

    /**
     * Returns the detailed information of the user.
     *
     * <p>
     * If the user identifier is not present in the ldap store an {@link IOException} will be throw.
     * </p>
     * <p>
     * URL Format: [BASE_MAPPING]/users/{uid}
     * </p>
     * <p>
     * Example: [BASE_MAPPING]/users/hsimpson
     * </p>
     *
     */
    @RequestMapping(value = REQUEST_MAPPING
            + "/{uid:.+}", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
    @ResponseBody
    public AccountImpl findByUid(@PathVariable String uid)
            throws AccessDeniedException, NameNotFoundException, DataServiceException {

        // Check for protected accounts
        if (this.userRule.isProtected(uid))
            throw new AccessDeniedException("The user is protected: " + uid);

        // Check delegation
        this.checkAuthorization(uid);

        return (AccountImpl) this.accountDao.findByUID(uid);

    }

    /**
     * Returns the profile of current user.
     *
     * <p>
     * URL Format: [BASE_MAPPING]/users/profile
     * </p>
     *
     * returns following format :
     *
     * <pre>
     * {
     *   uid: "testuser",
     *   org: "psc",
     *   roles: ["USER", "MOD_EXTRACTORAPP"]
     * }
     * </pre>
     */
    @RequestMapping(value = REQUEST_MAPPING
            + "/profile", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
    @ResponseBody
    public String myProfile(HttpServletRequest request) throws DataServiceException, JSONException {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        Account user = this.accountDao.findByUID(auth.getName());

        JSONArray roles = new JSONArray();
        for (Role role : this.roleDao.findAllForUser(auth.getName()))
            roles.put(role.getName());
        JSONObject res = new JSONObject();
        res.put("uid", auth.getName());
        res.put("roles", roles);
        res.put("org", user.getOrg());

        return res.toString();
    }

    /**
     * <p>
     * Creates a new user.
     * </p>
     *
     * <pre>
     * <b>Request</b>
     *
     * user data:
     * {
      *  "sn": "surname",
      *   "givenName": "first name",
      *   "mail": "e-mail",
      *    "telephoneNumber": "telephone"
      *   "facsimileTelephoneNumber": "value",
      *    "street": "street",
      *    "postalCode": "postal code",
      *   "l": "locality",
      *    "postOfficeBox": "the post office box",
      *  "org": "the_organization"
      * }
      *
      * where <b>sn, givenName, mail</b> are mandatories
     * </pre>
     * <pre>
     * <b>Response</b>
     *
     * <b>- Success case</b>
     *
     * The generated uid is added to the user data. So, a succeeded response should look like:
     * {
     *    <b>"uid": generated uid</b>
     *
      *  "sn": "surname",
      *   "givenName": "first name",
      *   "mail": "e-mail",
      *    "telephoneNumber": "telephone"
      *   "facsimileTelephoneNumber": "value",
      *    "street": "street",
      *    "postalCode": "postal code",
      *   "l": "locality",
      *    "postOfficeBox": "the post office box"
      * }
     * </pre>
     *
     * <pre>
     * <b>- Error case</b>
     * If the provided e-mail exists in the LDAP store the response will contain:
     *
     *    { \"success\": false, \"error\": \"duplicated_email\"}
     *
     * Error: 409 conflict with the current state of resource
     *
     * </pre>
     *
     * @param request HTTP POST data contains the user data
     * @throws IOException
     */
    @RequestMapping(value = REQUEST_MAPPING, method = RequestMethod.POST, produces = "application/json; charset=utf-8")
    @ResponseBody
    public Account create(HttpServletRequest request)
            throws IOException, DuplicatedEmailException, DataServiceException, DuplicatedUidException {

        Account account = createAccountFromRequestBody(request.getInputStream());
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        // Verify that org is under delegation if user is not SUPERUSER
        if (!auth.getAuthorities().contains(this.advancedDelegationDao.ROLE_SUPERUSER)) {
            DelegationEntry delegation = this.delegationDao.findOne(auth.getName());
            if (!Arrays.asList(delegation.getOrgs()).contains(account.getOrg()))
                throw new AccessDeniedException("Org not under delegation");
        }

        if (this.userRule.isProtected(account.getUid()))
            throw new AccessDeniedException("The user is protected: " + account.getUid());

        // Saves the user in the LDAP
        this.accountDao.insert(account, Role.USER, auth.getName());

        return account;
    }

    /**
     * Modifies the user data using the fields provided in the request body.
     * <p>
     * The fields that are not present in the parameters will remain untouched in the LDAP store.
     * </p>
     * <p>
     * The request format is:
     * [BASE_MAPPING]/users/{uid}
     * </p>
     * <p>
     * The request body should contains a the fields to modify using the JSON syntax.
     * </p>
     * <p>
     * Example:
     * </p>
     * <pre>
     * <b>Request</b>
     * [BASE_MAPPING]/users/hsimpson
     *
     * <b>Body request: </b>
     * {"sn": "surname",
     *  "givenName": "first name",
     *  "mail": "e-mail",
     *  "telephoneNumber": "telephone",
     *  "facsimileTelephoneNumber": "value",
      *    "street": "street",
      *  "postalCode": "postal code",
      *  "l": "locality",
      *  "postOfficeBox": "the post office box"
      * }
     *
     * </pre>
     * @param request
     *
     * @throws IOException if the uid does not exist or fails to access to the LDAP store.
     * @throws NameNotFoundException
     */
    @RequestMapping(value = REQUEST_MAPPING
            + "/{uid:.+}", method = RequestMethod.PUT, produces = "application/json; charset=utf-8")
    @ResponseBody
    public Account update(@PathVariable String uid, HttpServletRequest request) throws IOException,
            NameNotFoundException, DataServiceException, DuplicatedEmailException, ParseException, JSONException {

        if (this.userRule.isProtected(uid))
            throw new AccessDeniedException("The user is protected, it cannot be updated: " + uid);

        // check if user is under delegation for delegated admins
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        this.checkAuthorization(uid);

        // searches the account
        Account account = this.accountDao.findByUID(uid);
        String originalOrg = account.getOrg();

        // modifies the account data
        final Account modified = modifyAccount(AccountFactory.create(account), request.getInputStream());

        if (!modified.getOrg().equals(originalOrg)) {
            if (!auth.getAuthorities().contains(ROLE_SUPERUSER))
                if (!Arrays.asList(this.delegationDao.findOne(auth.getName()).getOrgs())
                        .contains(modified.getOrg()))
                    throw new AccessDeniedException("User not under delegation");
            if (originalOrg.length() > 0)
                this.orgDao.removeUser(originalOrg, uid);
            if (modified.getOrg().length() > 0)
                this.orgDao.addUser(modified.getOrg(), uid);
        }

        // Finally store account in LDAP
        this.accountDao.update(account, modified, auth.getName());

        boolean uidChanged = (!modified.getUid().equals(account.getUid()));
        if ((uidChanged) && (warnUserIfUidModified)) {
            this.mailService.sendAccountUidRenamed(request.getSession().getServletContext(), modified.getUid(),
                    modified.getCommonName(), modified.getEmail());
        }
        return modified;
    }

    /**
     * Deletes the user.
     *
     * The request format is:
     * <pre>
     * [BASE_MAPPING]/users/{uid}
     * </pre>
     *
     * @param request
     * @param response
     * @throws IOException
     */
    @RequestMapping(value = REQUEST_MAPPING + "/{uid:.+}", method = RequestMethod.DELETE)
    public void delete(@PathVariable String uid, HttpServletRequest request, HttpServletResponse response)
            throws IOException, DataServiceException {

        if (this.userRule.isProtected(uid))
            throw new AccessDeniedException("The user is protected, it cannot be deleted: " + uid);

        // check if user is under delegation for delegated admins
        this.checkAuthorization(uid);

        this.accountDao.delete(uid, request.getHeader("sec-username"));

        // Also delete delegation if exists
        if (this.delegationDao.findOne(uid) != null)
            this.delegationDao.delete(uid);

        ResponseUtil.writeSuccess(response);
    }

    /**
      * Return a list of required fields for user creation
      *
      * return a JSON array with required fields.
      */
    @RequestMapping(value = PUBLIC_REQUEST_MAPPING + "/requiredFields", method = RequestMethod.GET)
    public void getUserRequiredFields(HttpServletResponse response) throws IOException {
        try {
            JSONArray fields = new JSONArray();
            fields.put(UserSchema.UID_KEY);
            fields.put(UserSchema.MAIL_KEY);
            fields.put(UserSchema.SURNAME_KEY);
            fields.put(UserSchema.GIVEN_NAME_KEY);
            ResponseUtil.buildResponse(response, fields.toString(4), HttpServletResponse.SC_OK);
        } catch (Exception e) {
            LOG.error(e.getMessage());
            ResponseUtil.buildResponse(response, ResponseUtil.buildResponseMessage(false, e.getMessage()),
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            throw new IOException(e);
        }
    }

    /**
     * Modify only the account's fields that are present in the request body.
     *
     * @param account
     * @param inputStream
     *
     * @return the modified account
     *
     * @throws IOException
     */
    private Account modifyAccount(Account account, ServletInputStream inputStream)
            throws IOException, JSONException, ParseException {

        String strUser = FileUtils.asString(inputStream);
        JSONObject json = new JSONObject(strUser);

        String givenName = RequestUtil.getFieldValue(json, UserSchema.GIVEN_NAME_KEY);
        if (givenName != null) {
            account.setGivenName(givenName);
        }

        String surname = RequestUtil.getFieldValue(json, UserSchema.SURNAME_KEY);
        if (surname != null) {
            account.setSurname(surname);
        }

        String email = RequestUtil.getFieldValue(json, UserSchema.MAIL_KEY);
        if (email != null) {
            account.setEmail(email);
        }

        String postalAddress = RequestUtil.getFieldValue(json, UserSchema.POSTAL_ADDRESS_KEY);
        if (postalAddress != null) {
            account.setPostalAddress(postalAddress);
        }

        String postOfficeBox = RequestUtil.getFieldValue(json, UserSchema.POST_OFFICE_BOX_KEY);
        if (postOfficeBox != null) {
            account.setPostOfficeBox(postOfficeBox);
        }

        String postalCode = RequestUtil.getFieldValue(json, UserSchema.POSTAL_CODE_KEY);
        if (postalCode != null) {
            account.setPostalCode(postalCode);
        }

        String street = RequestUtil.getFieldValue(json, UserSchema.STREET_KEY);
        if (street != null) {
            account.setStreet(street);
        }

        String locality = RequestUtil.getFieldValue(json, UserSchema.LOCALITY_KEY);
        if (locality != null) {
            account.setLocality(locality);
        }

        String phone = RequestUtil.getFieldValue(json, UserSchema.TELEPHONE_KEY);
        if (phone != null) {
            account.setPhone(phone);
        }

        String facsimile = RequestUtil.getFieldValue(json, UserSchema.FACSIMILE_KEY);
        if (facsimile != null) {
            account.setFacsimile(facsimile);
        }

        String title = RequestUtil.getFieldValue(json, UserSchema.TITLE_KEY);
        if (title != null) {
            account.setTitle(title);
        }

        String description = RequestUtil.getFieldValue(json, UserSchema.DESCRIPTION_KEY);
        if (description != null) {
            account.setDescription(description);
        }

        String manager = RequestUtil.getFieldValue(json, UserSchema.MANAGER_KEY);
        account.setManager(manager);

        String context = RequestUtil.getFieldValue(json, UserSchema.CONTEXT_KEY);
        if (context != null) {
            account.setContext(context);
        }

        String commonName = AccountFactory.formatCommonName(account.getGivenName(), account.getSurname());
        account.setCommonName(commonName);

        String uid = RequestUtil.getFieldValue(json, UserSchema.UID_KEY);
        if (uid != null) {
            account.setUid(uid);
        }

        String org = RequestUtil.getFieldValue(json, UserSchema.ORG_KEY);
        if (org != null)
            account.setOrg(org);

        String shadowExpire = RequestUtil.getFieldValue(json, UserSchema.SHADOW_EXPIRE_KEY);
        if (shadowExpire != null) {
            if ("".equals(shadowExpire))
                account.setShadowExpire(null);
            else
                account.setShadowExpire((new SimpleDateFormat("yyyy-MM-dd")).parse(shadowExpire));
        }

        return account;
    }

    /**
     * Create a new account from the body request.
     *
     * @param is
     * @return
     * @throws IOException
     */
    private Account createAccountFromRequestBody(ServletInputStream is)
            throws IllegalArgumentException, IOException {

        JSONObject json;
        try {
            json = new JSONObject(FileUtils.asString(is));
        } catch (JSONException e) {
            LOG.error(e.getMessage());
            throw new IOException(e);
        }

        String givenName = RequestUtil.getFieldValue(json, UserSchema.GIVEN_NAME_KEY);
        String surname = RequestUtil.getFieldValue(json, UserSchema.SURNAME_KEY);
        String email = RequestUtil.getFieldValue(json, UserSchema.MAIL_KEY);
        String postalAddress = RequestUtil.getFieldValue(json, UserSchema.POSTAL_ADDRESS_KEY);
        String postOfficeBox = RequestUtil.getFieldValue(json, UserSchema.POST_OFFICE_BOX_KEY);
        String postalCode = RequestUtil.getFieldValue(json, UserSchema.POSTAL_CODE_KEY);
        String street = RequestUtil.getFieldValue(json, UserSchema.STREET_KEY);
        String locality = RequestUtil.getFieldValue(json, UserSchema.LOCALITY_KEY);
        String phone = RequestUtil.getFieldValue(json, UserSchema.TELEPHONE_KEY);
        String facsimile = RequestUtil.getFieldValue(json, UserSchema.FACSIMILE_KEY);
        String title = RequestUtil.getFieldValue(json, UserSchema.TITLE_KEY);
        String description = RequestUtil.getFieldValue(json, UserSchema.DESCRIPTION_KEY);
        String manager = RequestUtil.getFieldValue(json, UserSchema.MANAGER_KEY);
        String context = RequestUtil.getFieldValue(json, UserSchema.CONTEXT_KEY);
        String org = RequestUtil.getFieldValue(json, UserSchema.ORG_KEY);

        if (givenName == null)
            throw new IllegalArgumentException("First Name is required");

        if (surname == null)
            throw new IllegalArgumentException("Last Name is required");

        if (email == null)
            throw new IllegalArgumentException("EMail is required");

        // Use specified login if not empty
        String uid = RequestUtil.getFieldValue(json, UserSchema.UID_KEY);
        if (!StringUtils.hasLength(uid))
            try {
                uid = createUid(givenName, surname);
            } catch (DataServiceException e) {
                LOG.error(e.getMessage());
                throw new IOException(e);
            }

        String commonName = AccountFactory.formatCommonName(givenName, surname);

        Account a = AccountFactory.createFull(uid, commonName, surname, givenName, email, title, phone, description,
                postalAddress, postalCode, "", postOfficeBox, "", street, locality, facsimile, "", "", "", "",
                manager, context, org);

        String shadowExpire = RequestUtil.getFieldValue(json, UserSchema.SHADOW_EXPIRE_KEY);
        if (StringUtils.hasLength(shadowExpire)) {
            try {
                a.setShadowExpire((new SimpleDateFormat("yyyy-MM-dd")).parse(shadowExpire));
            } catch (ParseException e) {
                LOG.error(e.getMessage());
                throw new IllegalArgumentException(e);
            }
        }

        return a;
    }

    /**
     * Creates a uid based on the given name and surname
     *
     * @param givenName
     * @param surname
     * @return return the proposed uid
     *
     * @throws DataServiceException
     */
    private String createUid(String givenName, String surname) throws DataServiceException {

        String proposedUid = normalizeString(givenName.toLowerCase().charAt(0) + surname.toLowerCase());

        if (!this.accountDao.exist(proposedUid)) {
            return proposedUid;
        } else {
            return this.accountDao.generateUid(proposedUid);
        }
    }

    /**
     * Check Authorization of current logged user against specified uid and throw a AccessDeniedException
     * if current user is not SUPERUSER and user 'uid' is not under the delegation.
     * @param uid Identifier of user to search in delegation of connected user
     *
     * @throws AccessDeniedException if current user does not have permission to edit user 'uid'
     */
    private void checkAuthorization(String uid) {
        // check if user is under delegation for delegated admins
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (!auth.getAuthorities().contains(this.advancedDelegationDao.ROLE_SUPERUSER))
            if (!this.advancedDelegationDao.findUsersUnderDelegation(auth.getName()).contains(uid))
                throw new AccessDeniedException("User " + uid + " not under delegation");
    }

    /**
     * Deaccentuate a string and remove non-word characters
     *
     * references: http://stackoverflow.com/a/8523728 and
     * http://stackoverflow.com/a/2397830
     *
     * @param string an accentuated string, eg. "Jo+o"
     * @return return the deaccentuated string, eg. "Joao"
     */
    public static String normalizeString(String string) {
        return Normalizer.normalize(string, Normalizer.Form.NFD).replaceAll("\\W", "");
    }
}