Skip to content

Commit

Permalink
Merge "CFID-402: /Groups/{group}/Users endpoint" into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
dsyer authored and Gerrit Code Review committed Sep 5, 2012
2 parents 16e57bd + f83e29f commit 90ffecf
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 12 deletions.
Expand Up @@ -205,7 +205,6 @@ public ImplicitResourceDetails getImplicitResource(String clientPrefix, String d
resource.setId(clientId);
resource.setClientAuthenticationScheme(AuthenticationScheme.header);
resource.setAccessTokenUri(server.getAuthorizationUri());
resource.setScope(Arrays.asList("cloud_controller.read", "password.write", "openid"));
String redirectUri = environment.getProperty(clientPrefix + ".redirect-uri", defaultRedirectUri);
resource.setPreEstablishedRedirectUri(redirectUri);
return resource;
Expand Down
@@ -0,0 +1,134 @@
/*
* 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.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.cloudfoundry.identity.uaa.security.DefaultSecurityContextAccessor;
import org.cloudfoundry.identity.uaa.security.SecurityContextAccessor;
import org.cloudfoundry.identity.uaa.user.UaaAuthority;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.ExceptionHandler;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.View;

/**
* @author Dave Syer
*
*/
@Controller
public class GroupsUsersEndpoints implements InitializingBean {

private SecurityContextAccessor securityContextAccessor = new DefaultSecurityContextAccessor();

private ScimUserEndpoints scimUserEndpoints;

private Set<Pattern> patterns = new HashSet<Pattern>();

{
patterns.add(Pattern.compile("(.*?)([a-z0-9]*) eq (.*?)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
patterns.add(Pattern.compile("(.*?)([a-z0-9]*) co (.*?)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
patterns.add(Pattern.compile("(.*?)([a-z0-9]*) sw (.*?)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
patterns.add(Pattern.compile("(.*?)([a-z0-9]*) gt (.*?)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
patterns.add(Pattern.compile("(.*?)([a-z0-9]*) ge (.*?)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
patterns.add(Pattern.compile("(.*?)([a-z0-9]*) lt (.*?)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
patterns.add(Pattern.compile("(.*?)([a-z0-9]*) le (.*?)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
patterns.add(Pattern.compile("pr (.*?)([a-z0-9]*)([\\s]*.*)", Pattern.CASE_INSENSITIVE));
}

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

/**
* @param scimUserEndpoints the scimUserEndpoints to set
*/
public void setScimUserEndpoints(ScimUserEndpoints scimUserEndpoints) {
this.scimUserEndpoints = scimUserEndpoints;
}

@RequestMapping(value = "/Groups/{group}/Users", method = RequestMethod.GET)
@ResponseBody
public SearchResults<Map<String, Object>> findUsers(@PathVariable String group,
@RequestParam(required = false, defaultValue = "") String filter,
@RequestParam(required = false, defaultValue = "ascending") String sortOrder,
@RequestParam(required = false, defaultValue = "1") int startIndex,
@RequestParam(required = false, defaultValue = "100") int count) {
checkFilter(filter);
checkGroup(group);
String appended = filter.trim();
appended = (appended.length() > 0 ? "(" : "") + appended + (appended.length() > 0 ? ") and " : "")
+ "groups.display co '" + group + "'";
return scimUserEndpoints.findUsers("id,userName", appended, "userName", sortOrder, startIndex, count);
}

@ExceptionHandler
public View handleException(Exception t, HttpServletRequest request) throws ScimException {
return scimUserEndpoints.handleException(t, request);
}

private void checkFilter(String filter) {
String lowerCase = filter.toLowerCase();
if (lowerCase.contains("groups.")) {
throw new ScimException(
"Invalid filter expression: [" + filter + "] (no group filters allowed on /Groups)",
HttpStatus.BAD_REQUEST);
}
for (Pattern pattern : patterns) {
Matcher matcher = pattern.matcher(lowerCase);
if (matcher.matches()) {
String field = matcher.group(2);
if (!"username".equals(field) && !"id".equals(field)) {
throw new ScimException("Invalid filter expression: [" + filter + "] (no " + field
+ " filters allowed on /Groups)", HttpStatus.BAD_REQUEST);
}
}
}
}

private void checkGroup(String group) {
if (securityContextAccessor.isClient() || securityContextAccessor.isAdmin()) {
return;
}
if (UaaAuthority.UAA_USER.toString().equals(group)) {
throw new ScimException("Current user is not allowed to query group: " + group, HttpStatus.FORBIDDEN);
}
Collection<? extends GrantedAuthority> authorities = securityContextAccessor.getAuthorities();
Set<String> values = AuthorityUtils.authorityListToSet(authorities);
if (!values.contains(group)) {
throw new ScimException("Current user is not in requested group: " + group, HttpStatus.FORBIDDEN);
}
}

@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(scimUserEndpoints, "ScimUserEndpoints must be set");
}
}
Expand Up @@ -363,6 +363,6 @@ void setSecurityContextAccessor(SecurityContextAccessor securityContextAccessor)

@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(dao, "Dao must be set");
Assert.notNull(dao, "ScimUserProvisioning must be set");
}
}
@@ -0,0 +1,87 @@
/*
* 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 static org.junit.internal.matchers.StringContains.containsString;

import java.util.Collection;

import org.cloudfoundry.identity.uaa.security.SecurityContextAccessor;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.springframework.security.core.authority.AuthorityUtils;

/**
* @author Dave Syer
*
*/
public class GroupsUsersEndpointsTests {

@Rule
public ExpectedException expected = ExpectedException.none();

private GroupsUsersEndpoints endpoints = new GroupsUsersEndpoints();

private SecurityContextAccessor securityContextAccessor = Mockito.mock(SecurityContextAccessor.class);

private ScimUserEndpoints scimUserEndpoints = Mockito.mock(ScimUserEndpoints.class);

@SuppressWarnings("rawtypes")
private Collection authorities = (Collection) AuthorityUtils.commaSeparatedStringToAuthorityList("orgs.foo,uaa.user");

@SuppressWarnings("unchecked")
@Before
public void init() {
endpoints.setSecurityContextAccessor(securityContextAccessor);
endpoints.setScimUserEndpoints(scimUserEndpoints);
Mockito.when(securityContextAccessor.getAuthorities()).thenReturn(authorities);
}

@Test
public void testDefaultFilterHappyDay() {
endpoints.findUsers("orgs.foo", "", "ascending", 0, 100);
}

@Test
public void testDefaultFilterWrongGroup() {
expected.expect(ScimException.class);
expected.expectMessage(containsString("Current user"));
endpoints.findUsers("orgs.bar", "", "ascending", 0, 100);
}

@Test
public void testBadFieldInFilter() {
expected.expect(ScimException.class);
expected.expectMessage(containsString("Invalid filter"));
endpoints.findUsers("orgs.foo", "emails.value eq 'foo@bar.org'", "ascending", 0, 100);
}

@Test
public void testBadFilterWithGroup() {
expected.expect(ScimException.class);
expected.expectMessage(containsString("Invalid filter"));
endpoints.findUsers("orgs.foo", "groups.display co 'foo'", "ascending", 0, 100);
}

@Test
public void testBadGroup() {
expected.expect(ScimException.class);
expected.expectMessage(containsString("Current user"));
endpoints.findUsers("uaa.user", "", "ascending", 0, 100);
}

}
4 changes: 2 additions & 2 deletions uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml
Expand Up @@ -36,7 +36,7 @@
<map>
<entry key="id" value="vmc" />
<entry key="authorized-grant-types" value="implicit" />
<entry key="scope" value="cloud_controller.read,cloud_controller.write,openid,password.write,tokens.read,tokens.write" />
<entry key="scope" value="cloud_controller.read,cloud_controller.write,openid,password.write,tokens.read,tokens.write,scim.userids" />
<entry key="authorities" value="uaa.none" />
</map>
</entry>
Expand All @@ -45,7 +45,7 @@
<entry key="id" value="app" />
<entry key="secret" value="appclientsecret" />
<entry key="authorized-grant-types" value="password,authorization_code,refresh_token,client_credentials" />
<entry key="scope" value="cloud_controller.read,cloud_controller.write,openid,password.write,tokens.read,tokens.write" />
<entry key="scope" value="cloud_controller.read,cloud_controller.write,openid,password.write,tokens.read,tokens.write,scim.userids" />
<entry key="authorities" value="uaa.resource" />
</map>
</entry>
Expand Down
1 change: 1 addition & 0 deletions uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml
Expand Up @@ -134,6 +134,7 @@
<value>password.write</value>
<value>tokens.write</value>
<value>tokens.read</value>
<value>scim.userids</value>
</set>
</property>
</bean>
Expand Down
12 changes: 12 additions & 0 deletions uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml
Expand Up @@ -44,6 +44,10 @@
</property>
</bean>

<bean id="groupsUsersEndpoints" class="org.cloudfoundry.identity.uaa.scim.GroupsUsersEndpoints">
<property name="scimUserEndpoints" ref="scimUserEndpoints" />
</bean>

<http pattern="/User/*/password" create-session="stateless" authentication-manager-ref="emptyAuthenticationManager"
entry-point-ref="oauthAuthenticationEntryPoint" access-decision-manager-ref="accessDecisionManager"
xmlns="http://www.springframework.org/schema/security">
Expand All @@ -64,6 +68,14 @@
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

<http pattern="/Groups/*/Users" create-session="stateless" authentication-manager-ref="emptyAuthenticationManager"
entry-point-ref="oauthAuthenticationEntryPoint" access-decision-manager-ref="accessDecisionManager"
xmlns="http://www.springframework.org/schema/security">
<intercept-url pattern="/User/*/password" access="IS_AUTHENTICATED_FULLY,scope=scim.userids" />
<custom-filter ref="scimResourceAuthenticationFilter" position="PRE_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

<oauth:resource-server id="passwordResourceAuthenticationFilter" token-services-ref="tokenServices"
resource-id="password" entry-point-ref="oauthAuthenticationEntryPoint" />

Expand Down

0 comments on commit 90ffecf

Please sign in to comment.