org.cloudfoundry.identity.uaa.scim.ScimUserEndpoints.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudfoundry.identity.uaa.scim.ScimUserEndpoints.java

Source

/*
 * Cloud Foundry 2012.02.03 Beta
 * Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
 *
 * This product is licensed to you under the Apache License, Version 2.0 (the "License").
 * You may not use this product except in compliance with the License.
 *
 * This product includes a number of subcomponents with
 * separate copyright notices and license terms. Your use of these
 * subcomponents is subject to the terms and conditions of the
 * subcomponent's license, as noted in the LICENSE file.
 */
package org.cloudfoundry.identity.uaa.scim;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.error.ConvertingExceptionView;
import org.cloudfoundry.identity.uaa.error.ExceptionReport;
import org.cloudfoundry.identity.uaa.security.DefaultSecurityContextAccessor;
import org.cloudfoundry.identity.uaa.security.SecurityContextAccessor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.View;

/**
 * 
 * @author Luke Taylor
 * @author Dave Syer
 */
@Controller
public class ScimUserEndpoints implements InitializingBean {

    private final Log logger = LogFactory.getLog(getClass());

    private ScimUserProvisioning dao;

    private Collection<String> schemas = Arrays.asList(ScimUser.SCHEMAS);

    private static final Random passwordGenerator = new SecureRandom();

    private Map<Class<? extends Exception>, HttpStatus> statuses = new HashMap<Class<? extends Exception>, HttpStatus>();

    private HttpMessageConverter<?>[] messageConverters = new RestTemplate().getMessageConverters()
            .toArray(new HttpMessageConverter<?>[0]);

    private SecurityContextAccessor securityContextAccessor = new DefaultSecurityContextAccessor();

    /**
     * Set the message body converters to use.
     * <p>
     * These converters are used to convert from and to HTTP requests and responses.
     */
    public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
        this.messageConverters = messageConverters;
    }

    /**
     * Map from exception type to Http status.
     * 
     * @param statuses the statuses to set
     */
    public void setStatuses(Map<Class<? extends Exception>, HttpStatus> statuses) {
        this.statuses = statuses;
    }

    private static String generatePassword() {
        byte[] bytes = new byte[16];
        passwordGenerator.nextBytes(bytes);
        return new String(Hex.encode(bytes));
    }

    @RequestMapping(value = "/User/{userId}", method = RequestMethod.GET)
    @ResponseBody
    public ScimUser getUser(@PathVariable String userId) {
        return dao.retrieveUser(userId);
    }

    @RequestMapping(value = "/User", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public ScimUser createUser(@RequestBody ScimUser user) {
        return dao.createUser(user, user.getPassword() == null ? generatePassword() : user.getPassword());
    }

    @RequestMapping(value = "/User/{userId}", method = RequestMethod.PUT)
    @ResponseBody
    public ScimUser updateUser(@RequestBody ScimUser user, @PathVariable String userId,
            @RequestHeader(value = "If-Match", required = false, defaultValue = "NaN") String etag) {
        if (etag.equals("NaN")) {
            throw new ScimException("Missing If-Match for PUT", HttpStatus.BAD_REQUEST);
        }
        int version = getVersion(userId, etag);
        user.setVersion(version);
        try {
            return dao.updateUser(userId, user);
        } catch (OptimisticLockingFailureException e) {
            throw new ScimException(e.getMessage(), HttpStatus.CONFLICT);
        }
    }

    @RequestMapping(value = "/User/{userId}/password", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void changePassword(@PathVariable String userId, @RequestBody PasswordChangeRequest change) {
        checkPasswordChangeIsAllowed(userId, change.getOldPassword());

        if (!dao.changePassword(userId, change.getOldPassword(), change.getPassword())) {
            throw new ScimException("Password not changed for user: " + userId, HttpStatus.BAD_REQUEST);
        }
    }

    private void checkPasswordChangeIsAllowed(String userId, String oldPassword) {
        if (securityContextAccessor.isClient()) {
            // Trusted client (not acting on behalf of user)
            return;
        }

        // Call is by or on behalf of end user
        String currentUser = securityContextAccessor.getUserId();

        if (securityContextAccessor.isAdmin()) {

            // even an admin needs to provide the old value to change his password
            if (userId.equals(currentUser) && !StringUtils.hasText(oldPassword)) {
                throw new ScimException("Previous password is required even for admin", HttpStatus.BAD_REQUEST);
            }

        } else {

            if (!userId.equals(currentUser)) {
                logger.warn("User with id " + currentUser + " attempting to change password for user " + userId);
                // TODO: This should be audited when we have non-authentication events in the log
                throw new ScimException("Bad request. Not permitted to change another user's password",
                        HttpStatus.BAD_REQUEST);
            }

            // User is changing their own password, old password is required
            if (!StringUtils.hasText(oldPassword)) {
                throw new ScimException("Previous password is required", HttpStatus.BAD_REQUEST);
            }

        }

    }

    @RequestMapping(value = "/User/{userId}", method = RequestMethod.DELETE)
    @ResponseBody
    public ScimUser deleteUser(@PathVariable String userId,
            @RequestHeader(value = "If-Match", required = false) String etag) {
        int version = etag == null ? -1 : getVersion(userId, etag);

        return dao.removeUser(userId, version);
    }

    private int getVersion(String userId, String etag) {
        String value = etag.trim();
        if (value.equals("*")) {
            return dao.retrieveUser(userId).getVersion();
        }
        while (value.startsWith("\"")) {
            value = value.substring(1);
        }
        while (value.endsWith("\"")) {
            value = value.substring(0, value.length() - 1);
        }
        try {
            return Integer.valueOf(value);
        } catch (NumberFormatException e) {
            throw new ScimException("Invalid version match header (should be a version number): " + etag,
                    HttpStatus.BAD_REQUEST);
        }
    }

    @RequestMapping(value = "/Users", method = RequestMethod.GET)
    @ResponseBody
    public SearchResults<Map<String, Object>> findUsers(
            @RequestParam(value = "attributes", required = false, defaultValue = "id") String attributesCommaSeparated,
            @RequestParam(required = false, defaultValue = "id pr") String filter,
            @RequestParam(required = false) String sortBy,
            @RequestParam(required = false, defaultValue = "ascending") String sortOrder,
            @RequestParam(required = false, defaultValue = "1") int startIndex,
            @RequestParam(required = false, defaultValue = "100") int count) {

        List<ScimUser> input;
        try {
            input = dao.retrieveUsers(filter, sortBy, sortOrder.equals("ascending"));
        } catch (IllegalArgumentException e) {
            throw new ScimException("Invalid filter expression: [" + filter + "]", HttpStatus.BAD_REQUEST);
        }
        String[] attributes = attributesCommaSeparated.split(",");
        Map<String, Expression> expressions = new LinkedHashMap<String, Expression>();

        for (String attribute : attributes) {

            String spel = attribute.replaceAll("emails\\.(.*)", "emails.![$1]");
            logger.debug("Registering SpEL for attribute: " + spel);

            Expression expression;
            try {
                expression = new SpelExpressionParser().parseExpression(spel);
            } catch (SpelParseException e) {
                throw new ScimException("Invalid attributes: [" + attributesCommaSeparated + "]",
                        HttpStatus.BAD_REQUEST);
            }

            expressions.put(attribute, expression);

        }

        Collection<Map<String, Object>> users = new ArrayList<Map<String, Object>>();
        StandardEvaluationContext context = new StandardEvaluationContext();
        try {
            for (ScimUser user : input.subList(startIndex - 1, startIndex + count - 1)) {
                Map<String, Object> map = new LinkedHashMap<String, Object>();
                for (String attribute : expressions.keySet()) {
                    map.put(attribute, expressions.get(attribute).getValue(context, user));
                }
                users.add(map);
            }
        } catch (SpelEvaluationException e) {
            throw new ScimException("Invalid attributes: [" + attributesCommaSeparated + "]",
                    HttpStatus.BAD_REQUEST);
        }

        return new SearchResults<Map<String, Object>>(schemas, users, 1, count, input.size());

    }

    @ExceptionHandler
    public View handleException(Exception t, HttpServletRequest request) throws ScimException {
        ScimException e = new ScimException("Unexpected error", t, HttpStatus.INTERNAL_SERVER_ERROR);
        if (t instanceof ScimException) {
            e = (ScimException) t;
        } else if (t instanceof DataIntegrityViolationException) {
            e = new ScimException(t.getMessage(), t, HttpStatus.BAD_REQUEST);
        } else {
            Class<?> clazz = t.getClass();
            for (Class<?> key : statuses.keySet()) {
                if (key.isAssignableFrom(clazz)) {
                    e = new ScimException(t.getMessage(), t, statuses.get(key));
                    break;
                }
            }
        }
        // User can supply trace=true or just trace (unspecified) to get stack traces
        boolean trace = request.getParameter("trace") != null && !request.getParameter("trace").equals("false");
        return new ConvertingExceptionView(
                new ResponseEntity<ExceptionReport>(new ExceptionReport(e, trace), e.getStatus()),
                messageConverters);
    }

    public void setScimUserProvisioning(ScimUserProvisioning dao) {
        this.dao = dao;
    }

    void setSecurityContextAccessor(SecurityContextAccessor securityContextAccessor) {
        this.securityContextAccessor = securityContextAccessor;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(dao, "Dao must be set");
    }
}