org.cloudfoundry.identity.uaa.scim.endpoints.ScimUserEndpoints Maven / Gradle / Ivy
Show all versions of cloudfoundry-identity-scim Show documentation
/*******************************************************************************
* 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;
}
}