All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cloudfoundry.identity.uaa.scim.endpoints.ScimUserEndpoints Maven / Gradle / Ivy

There is a newer version: 2.7.4.9
Show newest version
/*******************************************************************************
 *     Cloud Foundry
 *     Copyright (c) [2009-2014] Pivotal Software, 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.endpoints;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCode;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore;
import org.cloudfoundry.identity.uaa.error.ConvertingExceptionView;
import org.cloudfoundry.identity.uaa.error.ExceptionReport;
import org.cloudfoundry.identity.uaa.oauth.approval.Approval;
import org.cloudfoundry.identity.uaa.oauth.approval.ApprovalStore;
import org.cloudfoundry.identity.uaa.rest.AttributeNameMapper;
import org.cloudfoundry.identity.uaa.rest.ResourceMonitor;
import org.cloudfoundry.identity.uaa.rest.SearchResults;
import org.cloudfoundry.identity.uaa.rest.SearchResultsFactory;
import org.cloudfoundry.identity.uaa.rest.SimpleAttributeNameMapper;
import org.cloudfoundry.identity.uaa.scim.ScimCore;
import org.cloudfoundry.identity.uaa.scim.ScimGroup;
import org.cloudfoundry.identity.uaa.scim.ScimGroupMembershipManager;
import org.cloudfoundry.identity.uaa.scim.ScimUser;
import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
import org.cloudfoundry.identity.uaa.scim.exception.ScimException;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException;
import org.cloudfoundry.identity.uaa.scim.exception.UserAlreadyVerifiedException;
import org.cloudfoundry.identity.uaa.scim.util.ScimUtils;
import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator;
import org.cloudfoundry.identity.uaa.util.UaaPagingUtils;
import org.cloudfoundry.identity.uaa.util.UaaStringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelParseException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.support.MetricType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
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;
/**
 * User provisioning and query endpoints. Implements the core API from the
 * Simple Cloud Identity Management (SCIM)
 * group. Exposes basic CRUD and query features for user accounts in a backend
 * database.
 *
 * @author Luke Taylor
 * @author Dave Syer
 *
 * @see SCIM specs
 */
@Controller
@ManagedResource
public class ScimUserEndpoints implements InitializingBean {
    private static final String USER_APPROVALS_FILTER_TEMPLATE = "user_id eq \"%s\"";

    private static Log logger = LogFactory.getLog(ScimUserEndpoints.class);

    public static final String E_TAG = "ETag";

    private ScimUserProvisioning dao;

    private ResourceMonitor scimUserResourceMonitor;

    private ScimGroupMembershipManager membershipManager;

    private ApprovalStore approvalStore;

    private static final Random passwordGenerator = new SecureRandom();

    private final Map errorCounts = new ConcurrentHashMap();

    private AtomicInteger scimUpdates = new AtomicInteger();

    private AtomicInteger scimDeletes = new AtomicInteger();

    private Map, HttpStatus> statuses = new HashMap, HttpStatus>();

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

    private PasswordValidator passwordValidator;

    private ExpiringCodeStore codeStore;

    /**
     * Set the message body converters to use.
     * 

* 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, HttpStatus> statuses) { this.statuses = statuses; } private static String generatePassword() { byte[] bytes = new byte[16]; passwordGenerator.nextBytes(bytes); return new String(Hex.encode(bytes)); } @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Total Users") public int getTotalUsers() { return scimUserResourceMonitor.getTotalCount(); } @ManagedMetric(metricType = MetricType.COUNTER, displayName = "User Account Update Count (Since Startup)") public int getUserUpdates() { return scimUpdates.get(); } @ManagedMetric(metricType = MetricType.COUNTER, displayName = "User Account Delete Count (Since Startup)") public int getUserDeletes() { return scimDeletes.get(); } @ManagedMetric(displayName = "Error Counts") public Map getErrorCounts() { return errorCounts; } @RequestMapping(value = "/Users/{userId}", method = RequestMethod.GET) @ResponseBody public ScimUser getUser(@PathVariable String userId, HttpServletResponse httpServletResponse) { ScimUser scimUser = syncApprovals(syncGroups(dao.retrieve(userId))); addETagHeader(httpServletResponse, scimUser); return scimUser; } @RequestMapping(value = "/Users", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) @ResponseBody public ScimUser createUser(@RequestBody ScimUser user, HttpServletResponse httpServletResponse) { if (user.getPassword() == null) { user.setPassword(generatePassword()); } else { passwordValidator.validate(user.getPassword()); } ScimUser scimUser = dao.createUser(user, user.getPassword()); if (user.getApprovals()!=null) { for (Approval approval : user.getApprovals()) { approval.setUserId(scimUser.getId()); approvalStore.addApproval(approval); } } scimUser = syncApprovals(syncGroups(scimUser)); addETagHeader(httpServletResponse, scimUser); return scimUser; } @RequestMapping(value = "/Users/{userId}", method = RequestMethod.PUT) @ResponseBody public ScimUser updateUser(@RequestBody ScimUser user, @PathVariable String userId, @RequestHeader(value = "If-Match", required = false, defaultValue = "NaN") String etag, HttpServletResponse httpServletResponse) { if (etag.equals("NaN")) { throw new ScimException("Missing If-Match for PUT", HttpStatus.BAD_REQUEST); } int version = getVersion(userId, etag); user.setVersion(version); try { ScimUser updated = dao.update(userId, user); scimUpdates.incrementAndGet(); ScimUser scimUser = syncApprovals(syncGroups(updated)); addETagHeader(httpServletResponse, scimUser); return scimUser; } catch (OptimisticLockingFailureException e) { throw new ScimResourceConflictException(e.getMessage()); } } @RequestMapping(value = "/Users/{userId}", method = RequestMethod.DELETE) @ResponseBody public ScimUser deleteUser(@PathVariable String userId, @RequestHeader(value = "If-Match", required = false) String etag, HttpServletResponse httpServletResponse) { int version = etag == null ? -1 : getVersion(userId, etag); ScimUser user = getUser(userId, httpServletResponse); membershipManager.removeMembersByMemberId(userId); dao.delete(userId, version); scimDeletes.incrementAndGet(); return user; } @RequestMapping(value = "/Users/{userId}/verify-link", method = RequestMethod.GET) @ResponseBody public ResponseEntity getUserVerificationLink(@PathVariable String userId, @RequestParam(value="client_id", required = false) String clientId, @RequestParam(value="redirect_uri") String redirectUri) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication instanceof OAuth2Authentication) { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication)authentication; if (clientId==null) { clientId = oAuth2Authentication.getOAuth2Request().getClientId(); } } VerificationResponse responseBody = new VerificationResponse(); ScimUser user = dao.retrieve(userId); if (user.isVerified()) { throw new UserAlreadyVerifiedException(); } ExpiringCode expiringCode = ScimUtils.getExpiringCode(codeStore, userId, user.getPrimaryEmail(), clientId, redirectUri); responseBody.setVerifyLink(ScimUtils.getVerificationURL(expiringCode)); return new ResponseEntity<>(responseBody, HttpStatus.OK); } @RequestMapping(value = "/Users/{userId}/verify", method = RequestMethod.GET) @ResponseBody public ScimUser verifyUser(@PathVariable String userId, @RequestHeader(value = "If-Match", required = false) String etag, HttpServletResponse httpServletResponse) { int version = etag == null ? -1 : getVersion(userId, etag); ScimUser user = dao.verifyUser(userId, version); scimUpdates.incrementAndGet(); addETagHeader(httpServletResponse, user); return user; } private int getVersion(String userId, String etag) { String value = etag.trim(); while (value.startsWith("\"")) { value = value.substring(1); } while (value.endsWith("\"")) { value = value.substring(0, value.length() - 1); } if (value.equals("*")) { return dao.retrieve(userId).getVersion(); } 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 findUsers( @RequestParam(value = "attributes", required = false) String attributesCommaSeparated, @RequestParam(required = false, defaultValue = "id pr") String filter, @RequestParam(required = false, defaultValue = "created") String sortBy, @RequestParam(required = false, defaultValue = "ascending") String sortOrder, @RequestParam(required = false, defaultValue = "1") int startIndex, @RequestParam(required = false, defaultValue = "100") int count) { if (startIndex < 1) { startIndex = 1; } List input = new ArrayList(); List result; try { result = dao.query(filter, sortBy, sortOrder.equals("ascending")); for (ScimUser user : UaaPagingUtils.subList(result, startIndex, count)) { if(attributesCommaSeparated == null || attributesCommaSeparated.matches("(?i)groups") || attributesCommaSeparated.isEmpty()) { syncGroups(user); } if(attributesCommaSeparated == null || attributesCommaSeparated.matches("(?i)approvals") || attributesCommaSeparated.isEmpty()) { syncApprovals(user); } input.add(user); } } catch (IllegalArgumentException e) { String msg = "Invalid filter expression: [" + filter + "]"; if (StringUtils.hasText(sortBy)) { msg += " [" +sortBy+"]"; } throw new ScimException(msg, HttpStatus.BAD_REQUEST); } if (!StringUtils.hasLength(attributesCommaSeparated)) { // Return all user data return new SearchResults(Arrays.asList(ScimCore.SCHEMAS), input, startIndex, count, result.size()); } AttributeNameMapper mapper = new SimpleAttributeNameMapper(Collections. singletonMap( "emails\\.(.*)", "emails.![$1]")); String[] attributes = attributesCommaSeparated.split(","); try { return SearchResultsFactory.buildSearchResultFrom(input, startIndex, count, result.size(), attributes, mapper, Arrays.asList(ScimCore.SCHEMAS)); } catch (SpelParseException e) { throw new ScimException("Invalid attributes: [" + attributesCommaSeparated + "]", HttpStatus.BAD_REQUEST); } catch (SpelEvaluationException e) { throw new ScimException("Invalid attributes: [" + attributesCommaSeparated + "]", HttpStatus.BAD_REQUEST); } } private ScimUser syncGroups(ScimUser user) { if (user == null) { return user; } Set directGroups = membershipManager.getGroupsWithMember(user.getId(), false); Set indirectGroups = membershipManager.getGroupsWithMember(user.getId(),true); indirectGroups.removeAll(directGroups); Set groups = new HashSet(); for (ScimGroup group : directGroups) { groups.add(new ScimUser.Group(group.getId(), group.getDisplayName(), ScimUser.Group.Type.DIRECT)); } for (ScimGroup group : indirectGroups) { groups.add(new ScimUser.Group(group.getId(), group.getDisplayName(), ScimUser.Group.Type.INDIRECT)); } user.setGroups(groups); return user; } private ScimUser syncApprovals(ScimUser user) { if (user == null || approvalStore == null) { return user; } Set approvals = new HashSet( approvalStore.getApprovals(String.format(USER_APPROVALS_FILTER_TEMPLATE, user.getId()))); Set active = new HashSet(approvals); for (Approval approval : approvals) { if (!approval.isCurrentlyActive()) { active.remove(approval); } } user.setApprovals(active); return user; } @ExceptionHandler public View handleException(Exception t, HttpServletRequest request) throws ScimException { logger.error("Unhandled exception in SCIM user endpoints.",t); ScimException e = new ScimException("Unexpected error", t, HttpStatus.INTERNAL_SERVER_ERROR); if (t instanceof ScimException) { e = (ScimException) t; } else { Class clazz = t.getClass(); for (Class key : statuses.keySet()) { if (key.isAssignableFrom(clazz)) { e = new ScimException(t.getMessage(), t, statuses.get(key)); break; } } } incrementErrorCounts(e); // 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<>(new ExceptionReport(e, trace, e.getExtraInfo()), e.getStatus()), messageConverters); } private void incrementErrorCounts(ScimException e) { String series = UaaStringUtils.getErrorName(e); AtomicInteger value = errorCounts.get(series); if (value == null) { synchronized (errorCounts) { value = errorCounts.get(series); if (value == null) { value = new AtomicInteger(); errorCounts.put(series, value); } } } value.incrementAndGet(); } public void setScimUserProvisioning(ScimUserProvisioning dao) { this.dao = dao; } public void setScimGroupMembershipManager(ScimGroupMembershipManager membershipManager) { this.membershipManager = membershipManager; } public void setApprovalStore(ApprovalStore approvalStore) { this.approvalStore = approvalStore; } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(dao, "ScimUserProvisioning must be set"); Assert.notNull(membershipManager, "ScimGroupMembershipManager must be set"); Assert.notNull(approvalStore, "ApprovalStore must be set"); } private void addETagHeader(HttpServletResponse httpServletResponse, ScimUser scimUser) { httpServletResponse.setHeader(E_TAG, "\"" + scimUser.getVersion() + "\""); } public void setScimUserResourceMonitor(ResourceMonitor scimUserResourceMonitor) { this.scimUserResourceMonitor = scimUserResourceMonitor; } public void setPasswordValidator(PasswordValidator passwordValidator) { this.passwordValidator = passwordValidator; } public void setCodeStore(ExpiringCodeStore codeStore) { this.codeStore = codeStore; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy