Browse Source

SEC-1490: Code for GAE Sample webapp

pull/1/head
Luke Taylor 16 years ago
parent
commit
5d35919ca3
  1. 2
      .gitignore
  2. 3
      build.gradle
  3. 7
      buildSrc/build.gradle
  4. 26
      buildSrc/src/main/groovy/gae/GaePlugin.groovy
  5. 1
      buildSrc/src/main/resources/META-INF/gradle-plugins/gae.properties
  6. 40
      samples/gae/gae.gradle
  7. 33
      samples/gae/src/main/java/samples/gae/security/AppRole.java
  8. 88
      samples/gae/src/main/java/samples/gae/security/GaeAuthenticationFilter.java
  9. 62
      samples/gae/src/main/java/samples/gae/security/GaeUserAuthentication.java
  10. 23
      samples/gae/src/main/java/samples/gae/security/GoogleAccountsAuthenticationEntryPoint.java
  11. 64
      samples/gae/src/main/java/samples/gae/security/GoogleAccountsAuthenticationProvider.java
  12. 93
      samples/gae/src/main/java/samples/gae/users/GaeDataStoreUserRegistry.java
  13. 92
      samples/gae/src/main/java/samples/gae/users/GaeUser.java
  14. 34
      samples/gae/src/main/java/samples/gae/users/InMemoryUserRegistry.java
  15. 18
      samples/gae/src/main/java/samples/gae/users/UserRegistry.java
  16. 25
      samples/gae/src/main/java/samples/gae/validation/Forename.java
  17. 18
      samples/gae/src/main/java/samples/gae/validation/ForenameValidator.java
  18. 25
      samples/gae/src/main/java/samples/gae/validation/Surname.java
  19. 18
      samples/gae/src/main/java/samples/gae/validation/SurnameValidator.java
  20. 48
      samples/gae/src/main/java/samples/gae/web/GaeAppController.java
  21. 61
      samples/gae/src/main/java/samples/gae/web/RegistrationController.java
  22. 31
      samples/gae/src/main/java/samples/gae/web/RegistrationForm.java
  23. 10
      samples/gae/src/main/webapp/WEB-INF/appengine-web.xml
  24. 48
      samples/gae/src/main/webapp/WEB-INF/applicationContext-security.xml
  25. 2
      samples/gae/src/main/webapp/WEB-INF/classes/ValidationMessages.properties
  26. 25
      samples/gae/src/main/webapp/WEB-INF/gae-servlet.xml
  27. 16
      samples/gae/src/main/webapp/WEB-INF/jsp/disabled.jsp
  28. 26
      samples/gae/src/main/webapp/WEB-INF/jsp/home.jsp
  29. 33
      samples/gae/src/main/webapp/WEB-INF/jsp/landing.jsp
  30. 17
      samples/gae/src/main/webapp/WEB-INF/jsp/loggedout.jsp
  31. 40
      samples/gae/src/main/webapp/WEB-INF/jsp/register.jsp
  32. 4
      samples/gae/src/main/webapp/WEB-INF/logging.properties
  33. 46
      samples/gae/src/main/webapp/WEB-INF/web.xml
  34. BIN
      samples/gae/src/main/webapp/favicon.ico
  35. 26
      samples/gae/src/main/webapp/static/css/gae.css
  36. 23
      samples/gae/src/test/java/samples/gae/security/AppRoleTests.java
  37. 53
      samples/gae/src/test/java/samples/gae/users/GaeDataStoreUserRegistryTests.java
  38. 3
      settings.gradle
  39. 4
      web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java

2
.gitignore vendored

@ -4,6 +4,7 @@ target/ @@ -4,6 +4,7 @@ target/
.project
.DS_Store
.settings/
out/
build/
*.log
*.iml
@ -11,3 +12,4 @@ build/ @@ -11,3 +12,4 @@ build/
*.iws
.gradle/
gradle.properties
atlassian-ide-plugin.xml

3
build.gradle

@ -37,6 +37,9 @@ allprojects { @@ -37,6 +37,9 @@ allprojects {
if (!config) {
return
}
ideaModule {
gradleCacheVariable = 'GRADLE_CACHE'
}
}
ideaModule {

7
buildSrc/build.gradle

@ -3,9 +3,11 @@ apply plugin: 'groovy' @@ -3,9 +3,11 @@ apply plugin: 'groovy'
repositories {
mavenRepo name:'localRepo', urls: "file://" + System.properties['user.home'] + "/.m2/repository"
mavenCentral()
mavenRepo name: 'GAE', urls:'http://maven-gae-plugin.googlecode.com/svn/repository'
mavenRepo name:'Shibboleth Repo', urls:'http://shibboleth.internet2.edu/downloads/maven2'
}
// Docbook Plugin
dependencies {
def fopDeps = [ 'org.apache.xmlgraphics:fop:0.95-1@jar',
'org.apache.xmlgraphics:xmlgraphics-commons:1.3',
@ -26,6 +28,11 @@ dependencies { @@ -26,6 +28,11 @@ dependencies {
'net.sf.docbook:docbook-xsl:1.75.2:ns-resources@zip'
}
// GAE
dependencies {
compile 'com.google.appengine:appengine-tools-api:1.3.5'
}
task ide(type: Copy) {
from configurations.runtime
into 'ide'

26
buildSrc/src/main/groovy/gae/GaePlugin.groovy

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
package gae;
import com.google.appengine.tools.admin.AppCfg
import org.gradle.api.*;
class GaePlugin implements Plugin<Project> {
public void apply(Project project) {
if (!project.hasProperty('appEngineSdkRoot')) {
println "'appEngineSdkRoot' must be set in gradle.properties"
}
System.setProperty('appengine.sdk.root', project.property('appEngineSdkRoot'))
File explodedWar = new File(project.buildDir, "gae-exploded")
project.task('gaeDeploy') << {
AppCfg.main("update", explodedWar.toString())
}
project.gaeDeploy.dependsOn project.war
project.war.doLast {
ant.unzip(src: project.war.archivePath, dest: explodedWar)
}
}
}

1
buildSrc/src/main/resources/META-INF/gradle-plugins/gae.properties

@ -0,0 +1 @@ @@ -0,0 +1 @@
implementation-class=gae.GaePlugin

40
samples/gae/gae.gradle

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
apply plugin: 'war'
apply plugin: 'jetty'
apply plugin: 'gae'
gaeVersion="1.3.5"
repositories {
// Hibernate Validator
mavenRepo name: 'JBoss', urls: 'https://repository.jboss.org/nexus/content/repositories/releases'
// GAE Jars
mavenRepo name: 'GAE', urls:'http://maven-gae-plugin.googlecode.com/svn/repository'
}
// Remove logback as it causes security issues with GAE.
configurations.runtime.exclude(group: 'ch.qos.logback')
dependencies {
providedCompile 'javax.servlet:servlet-api:2.5@jar',
"com.google.appengine:appengine-api-1.0-sdk:$gaeVersion"
compile project(':spring-security-core'),
project(':spring-security-web'),
"org.springframework:spring-beans:$springVersion",
"org.springframework:spring-web:$springVersion",
"org.springframework:spring-webmvc:$springVersion",
"org.springframework:spring-context:$springVersion",
"org.springframework:spring-context-support:$springVersion",
'javax.validation:validation-api:1.0.0.GA',
'org.hibernate:hibernate-validator:4.1.0.Final',
"org.slf4j:slf4j-api:$slf4jVersion"
runtime project(':spring-security-config'),
"org.slf4j:jcl-over-slf4j:$slf4jVersion",
"org.slf4j:slf4j-jdk14:$slf4jVersion"
testCompile "com.google.appengine:appengine-testing:$gaeVersion"
testRuntime "com.google.appengine:appengine-api-labs:$gaeVersion",
"com.google.appengine:appengine-api-stubs:$gaeVersion"
}

33
samples/gae/src/main/java/samples/gae/security/AppRole.java

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
package samples.gae.security;
import org.springframework.security.core.GrantedAuthority;
/**
* @author Luke Taylor
*/
public enum AppRole implements GrantedAuthority {
ADMIN (0),
NEW_USER (1),
USER (2);
private int bit;
/**
* Creates an authority with a specific bit representation. It's important that this doesn't
* change as it will be used in the database. The enum ordinal is less reliable as the enum may be
* reordered or have new roles inserted which would change the ordinal values.
*
* @param bit the permission bit which will represent this authority in the datastore.
*/
AppRole(int bit) {
this.bit = bit;
}
public int getBit() {
return bit;
}
public String getAuthority() {
return toString();
}
}

88
samples/gae/src/main/java/samples/gae/security/GaeAuthenticationFilter.java

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
package samples.gae.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserServiceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
/**
* @author Luke Taylor
*/
public class GaeAuthenticationFilter extends GenericFilterBean {
private static final String REGISTRATION_URL = "/register.htm";
private final Logger logger = LoggerFactory.getLogger(getClass());
private AuthenticationDetailsSource ads = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
User googleUser = UserServiceFactory.getUserService().getCurrentUser();
if (googleUser != null) {
logger.debug("Currently logged on to GAE as user " + googleUser);
logger.debug("Authenticating to Spring Security");
// User has returned after authenticating via GAE. Need to authenticate through Spring Security.
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(googleUser, null);
token.setDetails(ads.buildDetails(request));
try {
authentication = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {
failureHandler.onAuthenticationFailure((HttpServletRequest)request, (HttpServletResponse)response, e);
return;
}
}
}
// A new user has to register with the app before doing anything else
if (authentication != null && authentication.getAuthorities().contains(AppRole.NEW_USER)
&& !((HttpServletRequest)request).getRequestURI().endsWith(REGISTRATION_URL)) {
logger.debug("New user authenticated. Redirecting to registration page");
((HttpServletResponse) response).sendRedirect(REGISTRATION_URL);
return;
}
chain.doFilter(request, response);
}
@Override
public void afterPropertiesSet() throws ServletException {
Assert.notNull(authenticationManager, "AuthenticationManager must be set");
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
}

62
samples/gae/src/main/java/samples/gae/security/GaeUserAuthentication.java

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
package samples.gae.security;
import java.util.Collection;
import java.util.HashSet;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import samples.gae.users.GaeUser;
/**
* Authentication object representing a fully-authenticated user.
*
* @author Luke Taylor
*/
public class GaeUserAuthentication implements Authentication {
private final GaeUser principal;
private final Object details;
private boolean authenticated;
public GaeUserAuthentication(GaeUser principal, Object details) {
this.principal = principal;
this.details = details;
authenticated = true;
}
public Collection<GrantedAuthority> getAuthorities() {
return new HashSet<GrantedAuthority>(principal.getAuthorities());
}
public Object getCredentials() {
throw new UnsupportedOperationException();
}
public Object getDetails() {
return null;
}
public Object getPrincipal() {
return principal;
}
public boolean isAuthenticated() {
return authenticated;
}
public void setAuthenticated(boolean isAuthenticated) {
authenticated = isAuthenticated;
}
public String getName() {
return principal.getUserId();
}
@Override
public String toString() {
return "GaeUserAuthentication{" +
"principal=" + principal +
", details=" + details +
", authenticated=" + authenticated +
'}';
}
}

23
samples/gae/src/main/java/samples/gae/security/GoogleAccountsAuthenticationEntryPoint.java

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
package samples.gae.security;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
public class GoogleAccountsAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
UserService userService = UserServiceFactory.getUserService();
response.sendRedirect(userService.createLoginURL(request.getRequestURI()));
}
}

64
samples/gae/src/main/java/samples/gae/security/GoogleAccountsAuthenticationProvider.java

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
package samples.gae.security;
import com.google.appengine.api.users.User;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import samples.gae.users.GaeUser;
import samples.gae.users.UserRegistry;
/**
* A simple authentication provider which interacts with {@code User} returned by the GAE {@code UserService},
* and also the local persistent {@code UserRegistry} to build an application user principal.
* <p>
* If the user has been authenticated through google accounts, it will check if they are already registered
* and either load the existing user information or assign them a temporary identity with limited access until they
* have registered.
* <p>
* If the account has been disabled, a {@code DisabledException} will be raised.
*
* @author Luke Taylor
*/
public class GoogleAccountsAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserRegistry userRegistry;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
User googleUser = (User) authentication.getPrincipal();
GaeUser user = userRegistry.findUser(googleUser.getUserId());
if (user == null) {
// User not in registry. Needs to register
user = new GaeUser(googleUser.getUserId(), googleUser.getNickname(), googleUser.getEmail());
}
if (!user.isEnabled()) {
throw new DisabledException("Account is disabled");
}
return new GaeUserAuthentication(user, authentication.getDetails());
}
/**
* Indicate that this provider only supports PreAuthenticatedAuthenticationToken (sub)classes.
*/
public final boolean supports(Class<?> authentication) {
return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setUserRegistry(UserRegistry userRegistry) {
this.userRegistry = userRegistry;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
}

93
samples/gae/src/main/java/samples/gae/users/GaeDataStoreUserRegistry.java

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
package samples.gae.users;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Set;
import com.google.appengine.api.datastore.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import samples.gae.security.AppRole;
/**
* UserRegistry implementation which uses GAE's low-level Datastore APIs.
*
* @author Luke Taylor
*/
public class GaeDataStoreUserRegistry implements UserRegistry {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final String USER_TYPE = "GaeUser";
private static final String USER_FORENAME = "forename";
private static final String USER_SURNAME = "surname";
private static final String USER_NICKNAME = "nickname";
private static final String USER_EMAIL = "email";
private static final String USER_ENABLED = "enabled";
private static final String USER_AUTHORITIES = "authorities";
public GaeUser findUser(String userId) {
Key key = KeyFactory.createKey(USER_TYPE, userId);
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
try {
Entity user = datastore.get(key);
long binaryAuthorities = (Long)user.getProperty(USER_AUTHORITIES);
Set<AppRole> roles = EnumSet.noneOf(AppRole.class);
for (AppRole r : AppRole.values()) {
if ((binaryAuthorities & (1 << r.getBit())) != 0) {
roles.add(r);
}
}
GaeUser gaeUser = new GaeUser(
user.getKey().getName(),
(String)user.getProperty(USER_NICKNAME),
(String)user.getProperty(USER_EMAIL),
(String)user.getProperty(USER_FORENAME),
(String)user.getProperty(USER_SURNAME),
roles,
(Boolean)user.getProperty(USER_ENABLED));
return gaeUser;
} catch (EntityNotFoundException e) {
logger.debug(userId + " not found in datastore");
return null;
}
}
public void registerUser(GaeUser newUser) {
logger.debug("Attempting to create new user " + newUser);
Key key = KeyFactory.createKey(USER_TYPE, newUser.getUserId());
Entity user = new Entity(key);
user.setProperty(USER_EMAIL, newUser.getEmail());
user.setProperty(USER_NICKNAME, newUser.getNickname());
user.setProperty(USER_FORENAME, newUser.getForename());
user.setProperty(USER_SURNAME, newUser.getSurname());
user.setUnindexedProperty(USER_ENABLED, newUser.isEnabled());
Collection<? extends GrantedAuthority> roles = newUser.getAuthorities();
long binaryAuthorities = 0;
for (GrantedAuthority r : roles) {
binaryAuthorities |= 1 << ((AppRole)r).getBit();
}
user.setUnindexedProperty(USER_AUTHORITIES, binaryAuthorities);
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(user);
}
public void removeUser(String userId) {
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Key key = KeyFactory.createKey(USER_TYPE, userId);
datastore.delete(key);
}
}

92
samples/gae/src/main/java/samples/gae/users/GaeUser.java

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
package samples.gae.users;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import samples.gae.security.AppRole;
/**
* Custom user object for the application.
*
* @author Luke Taylor
*/
public class GaeUser implements Serializable {
private final String userId;
private final String email;
private final String nickname;
private final String forename;
private final String surname;
private final Set<AppRole> authorities;
private final boolean enabled;
/**
* Pre-registration constructor.
*
* Assigns the user the "NEW_USER" role only.
*/
public GaeUser(String userId, String nickname, String email) {
this.userId = userId;
this.nickname = nickname;
this.authorities = EnumSet.of(AppRole.NEW_USER);
this.forename = null;
this.surname = null;
this.email = email;
this.enabled = true;
}
/**
* Post-registration constructor
*/
public GaeUser(String userId, String nickname, String email, String forename, String surname, Set<AppRole> authorities, boolean enabled) {
this.userId = userId;
this.nickname = nickname;
this.email = email;
this.authorities = authorities;
this.forename = forename;
this.surname = surname;
this.enabled= enabled;
}
public String getUserId() {
return userId;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getForename() {
return forename;
}
public String getSurname() {
return surname;
}
public boolean isEnabled() {
return enabled;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String toString() {
return "GaeUser{" +
"userId='" + userId + '\'' +
", nickname='" + nickname + '\'' +
", forename='" + forename + '\'' +
", surname='" + surname + '\'' +
", authorities=" + authorities +
'}';
}
}

34
samples/gae/src/main/java/samples/gae/users/InMemoryUserRegistry.java

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
package samples.gae.users;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
/**
* @author Luke Taylor
*/
public class InMemoryUserRegistry implements UserRegistry {
private final Logger logger = LoggerFactory.getLogger(getClass());
private Map<String, GaeUser> users = Collections.synchronizedMap(new HashMap<String, GaeUser>());
public GaeUser findUser(String userId) {
return users.get(userId);
}
public void registerUser(GaeUser newUser) {
logger.debug("Attempting to create new user " + newUser);
Assert.state(!users.containsKey(newUser.getUserId()));
users.put(newUser.getUserId(), newUser);
}
public void removeUser(String userId) {
users.remove(userId);
}
}

18
samples/gae/src/main/java/samples/gae/users/UserRegistry.java

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
package samples.gae.users;
import com.google.appengine.api.datastore.EntityNotFoundException;
/**
*
* Service used to maintain a list of users who are registered with the application.
*
* @author Luke Taylor
*/
public interface UserRegistry {
GaeUser findUser(String userId);
void registerUser(GaeUser newUser);
void removeUser(String userId);
}

25
samples/gae/src/main/java/samples/gae/validation/Forename.java

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
package samples.gae.validation;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import org.hibernate.validator.constraints.NotBlank;
/**
* @author Luke Taylor
*/
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ForenameValidator.class)
public @interface Forename {
String message() default "{samples.gae.forename}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

18
samples/gae/src/main/java/samples/gae/validation/ForenameValidator.java

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
package samples.gae.validation;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @author Luke Taylor
*/
public class ForenameValidator implements ConstraintValidator<Forename, String> {
private static final Pattern VALID = Pattern.compile("[\\p{L}'\\-,.]+") ;
public void initialize(Forename constraintAnnotation) {}
public boolean isValid(String value, ConstraintValidatorContext context) {
return VALID.matcher(value).matches();
}
}

25
samples/gae/src/main/java/samples/gae/validation/Surname.java

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
package samples.gae.validation;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import org.hibernate.validator.constraints.NotBlank;
/**
* @author Luke Taylor
*/
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SurnameValidator.class)
public @interface Surname {
String message() default "{samples.gae.surname}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

18
samples/gae/src/main/java/samples/gae/validation/SurnameValidator.java

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
package samples.gae.validation;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @author Luke Taylor
*/
public class SurnameValidator implements ConstraintValidator<Surname, String> {
private static final Pattern VALID = Pattern.compile("[\\p{L}'\\-,.]+") ;
public void initialize(Surname constraintAnnotation) {}
public boolean isValid(String value, ConstraintValidatorContext context) {
return VALID.matcher(value).matches();
}
}

48
samples/gae/src/main/java/samples/gae/web/GaeAppController.java

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
package samples.gae.web;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.users.UserServiceFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
*
* @author Luke Taylor
*
*/
@Controller
public class GaeAppController {
@RequestMapping(value = "/", method= RequestMethod.GET)
public String landing() {
return "landing";
}
@RequestMapping(value = "/home.htm", method= RequestMethod.GET)
public String home() {
return "home";
}
@RequestMapping(value = "/disabled.htm", method= RequestMethod.GET)
public String disabled() {
return "disabled";
}
@RequestMapping(value = "/logout.htm", method= RequestMethod.GET)
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.getSession().invalidate();
String logoutUrl = UserServiceFactory.getUserService().createLogoutURL("/");
response.sendRedirect(logoutUrl);
}
@RequestMapping(value = "/loggedout.htm", method= RequestMethod.GET)
public String loggedOut() {
return "loggedout";
}
}

61
samples/gae/src/main/java/samples/gae/web/RegistrationController.java

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
package samples.gae.web;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import com.google.appengine.api.users.UserServiceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import samples.gae.security.AppRole;
import samples.gae.security.GaeUserAuthentication;
import samples.gae.users.GaeUser;
import samples.gae.users.UserRegistry;
/**
* @author Luke Taylor
*/
@Controller
@RequestMapping(value="/register.htm")
public class RegistrationController {
@Autowired
private UserRegistry registry;
@RequestMapping(method= RequestMethod.GET)
public RegistrationForm registrationForm() {
return new RegistrationForm();
}
@RequestMapping(method = RequestMethod.POST)
public String register(@Valid RegistrationForm form, BindingResult result) {
if (result.hasErrors()) {
return null;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
GaeUser currentUser = (GaeUser)authentication.getPrincipal();
Set<AppRole> roles = EnumSet.of(AppRole.USER);
if (UserServiceFactory.getUserService().isUserAdmin()) {
roles.add(AppRole.ADMIN);
}
GaeUser user = new GaeUser(currentUser.getUserId(), currentUser.getNickname(), currentUser.getEmail(),
form.getForename(), form.getSurname(), roles, true);
registry.registerUser(user);
// Update the context with the full authentication
SecurityContextHolder.getContext().setAuthentication(new GaeUserAuthentication(user, authentication.getDetails()));
return "redirect:/home.htm";
}
}

31
samples/gae/src/main/java/samples/gae/web/RegistrationForm.java

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
package samples.gae.web;
import org.hibernate.validator.constraints.NotBlank;
import samples.gae.validation.Forename;
import samples.gae.validation.Surname;
/**
* @author Luke Taylor
*/
public class RegistrationForm {
@Forename
private String forename;
@Surname
private String surname;
public String getForename() {
return forename;
}
public void setForename(String forename) {
this.forename = forename;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
}

10
samples/gae/src/main/webapp/WEB-INF/appengine-web.xml

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<application>gaespringsec</application>
<version>1</version>
<sessions-enabled>true</sessions-enabled>
<system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
</system-properties>
</appengine-web-app>

48
samples/gae/src/main/webapp/WEB-INF/applicationContext-security.xml

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<http pattern="/static/**" security="none" />
<http pattern="/favicon.ico" security="none" />
<http pattern="/_ah/resources**" security="none" />
<http use-expressions="true" entry-point-ref="gaeEntryPoint">
<intercept-url pattern="/" access="permitAll" />
<intercept-url pattern="/_ah/login**" access="permitAll" />
<intercept-url pattern="/_ah/admin**" access="permitAll" />
<intercept-url pattern="/logout.htm" access="permitAll" />
<intercept-url pattern="/register.htm*" access="hasRole('NEW_USER')" />
<intercept-url pattern="/**" access="hasRole('USER')" />
<custom-filter position="PRE_AUTH_FILTER" ref="gaeFilter" />
</http>
<b:bean id="gaeEntryPoint" class="samples.gae.security.GoogleAccountsAuthenticationEntryPoint" />
<b:bean id="gaeFilter" class="samples.gae.security.GaeAuthenticationFilter">
<b:property name="authenticationManager" ref="authenticationManager"/>
<b:property name="failureHandler">
<b:bean class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
<b:property name="exceptionMappings">
<b:map>
<b:entry key="org.springframework.security.authentication.DisabledException" value="/disabled.htm" />
</b:map>
</b:property>
</b:bean>
</b:property>
</b:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="gaeAuthenticationProvider"/>
</authentication-manager>
<b:bean id="gaeAuthenticationProvider" class="samples.gae.security.GoogleAccountsAuthenticationProvider">
<b:property name="userRegistry" ref="userRegistry" />
</b:bean>
<b:bean id="userRegistry" class="samples.gae.users.GaeDataStoreUserRegistry" />
</b:beans>

2
samples/gae/src/main/webapp/WEB-INF/classes/ValidationMessages.properties

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
samples.gae.forename=Must be a valid forename
samples.gae.surname=Must be a valid surname

25
samples/gae/src/main/webapp/WEB-INF/gae-servlet.xml

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<!-- Enables JSR-303 -->
<mvc:annotation-driven/>
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
<context:component-scan base-package="samples.gae"/>
<context:annotation-config />
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="messages"/>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>

16
samples/gae/src/main/webapp/WEB-INF/jsp/disabled.jsp

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@page session="false" %>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" />
<title>Disabled Account</title></head>
<body>
<div id="content">
<p>Sorry, it looks like your account has been disabled for some reason...</p>
</div>
</body>
</html>

26
samples/gae/src/main/webapp/WEB-INF/jsp/home.jsp

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" />
<title>Home Page</title>
</head>
<body>
<div id="content">
<h3>The Home Page</h3>
<p>Welcome back <sec:authentication property="principal.nickname"/>.</p>
<p>
You can get to this page if you have authenticated and are a registered user.
You are registered as
<sec:authentication property="principal.forename"/> <sec:authentication property="principal.surname"/>.
</p>
<p>
<a href="/logout.htm">Logout</a>.
</p>
</div>
</body>
</html>

33
samples/gae/src/main/webapp/WEB-INF/jsp/landing.jsp

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page session="false" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" />
<title>Spring Security GAE Sample</title>
</head>
<body>
<div id="content">
<h3>Spring Security GAE Application</h3>
<p>
This application demonstrates the integration of Spring Security
with the services provided by Google App Engine. It shows how to:
<ul>
<li>Authenticate using Google Accounts.</li>
<li>Implement "on&ndash;demand" authentication when a user accesses a secured resource.</li>
<li>Supplement the information from Google Accounts with application&ndash;specific roles.</li>
<li>Store user account data in an App Engine datastore using the native API.</li>
<li>Setup access-control restrictions based on the roles assigned to users.</li>
<li>Disable the accounts of specfic users to prevent access.</li>
</ul>
</p>
<p>
Go to the <a href="/home.htm">home page</a>.
</p>
</div>
</body>
</html>

17
samples/gae/src/main/webapp/WEB-INF/jsp/loggedout.jsp

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page session="false" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" />
<title>Spring Security GAE Sample</title>
</head>
<body>
<div id="content">
<p>You've been logged out of the application. <a href="/home.htm">Log back in</a>.</p>
</div>
</body>
</html>

40
samples/gae/src/main/webapp/WEB-INF/jsp/register.jsp

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" />
<title>Registration</title>
</head>
<body>
<div id="content">
<p>
Welcome to the Spring Security GAE sample application, <sec:authentication property="principal.nickname" />.
Please enter your registration details in order to use the application.
</p>
<p>
The data you enter here will be registered in the application's GAE data store, keyed under your unique
Google Accounts identifier. It doesn't have to be accurate. When you log in again, the information will be automatically
retrieved.
</p>
<form:form id="register" method="post" modelAttribute="registrationForm">
<fieldset>
<form:label path="forename">
Forename:
</form:label> <form:errors path="forename" cssClass="fieldError" /><br />
<form:input path="forename" /> <br />
<form:label path="surname">
Surname:
</form:label><form:errors path="surname" cssClass="fieldError" /> <br />
<form:input path="surname" /><br />
</fieldset>
<input type="submit" value="Register">
</form:form>
</body>
</div>
</html>

4
samples/gae/src/main/webapp/WEB-INF/logging.properties

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
.level = INFO
org.springframework.level = FINE
org.springframework.security.level = FINER

46
samples/gae/src/main/webapp/WEB-INF/web.xml

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext-security.xml
</param-value>
</context-param>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>gae</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/gae-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>gae</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>gae</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
</web-app>

BIN
samples/gae/src/main/webapp/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

26
samples/gae/src/main/webapp/static/css/gae.css

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
body {
font-family:"Palatino Linotype","Book Antiqua",Palatino,serif;
}
#content {
margin: 5em auto;
width: 40em;
}
form {
width: 25em;
margin: 0 2em;
}
form fieldset {
margin-bottom: 0.5em;
}
fieldset input {
margin: 0.6em 0;
}
.fieldError {
color: red;
}

23
samples/gae/src/test/java/samples/gae/security/AppRoleTests.java

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
package samples.gae.security;
import static org.junit.Assert.*;
import static samples.gae.security.AppRole.*;
import org.junit.Test;
import org.springframework.security.core.GrantedAuthority;
/**
* @author Luke Taylor
*/
public class AppRoleTests {
@Test
public void getAuthorityReturnsRoleName() {
GrantedAuthority admin = ADMIN;
assertEquals("ADMIN", admin.getAuthority());
}
}

53
samples/gae/src/test/java/samples/gae/users/GaeDataStoreUserRegistryTests.java

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
package samples.gae.users;
import static org.junit.Assert.assertEquals;
import java.util.EnumSet;
import java.util.Set;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import samples.gae.security.AppRole;
/**
* @author Luke Taylor
*/
public class GaeDataStoreUserRegistryTests {
private final LocalServiceTestHelper helper =
new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
@Before
public void setUp() throws Exception {
helper.setUp();
}
@After
public void tearDown() throws Exception {
helper.tearDown();
}
@Test
public void correctDataIsRetrievedAfterInsert() {
GaeDataStoreUserRegistry registry = new GaeDataStoreUserRegistry();
Set<AppRole> roles = EnumSet.of(AppRole.ADMIN, AppRole.USER);
String userId = "someUserId";
GaeUser origUser = new GaeUser(userId, "nick", "nick@blah.com", "Forename", "Surname", roles, true);
registry.registerUser(origUser);
GaeUser loadedUser = registry.findUser(userId);
assertEquals(loadedUser.getUserId(), origUser.getUserId());
assertEquals(true, loadedUser.isEnabled());
assertEquals(roles, loadedUser.getAuthorities());
assertEquals("nick", loadedUser.getNickname());
assertEquals("nick@blah.com", loadedUser.getEmail());
assertEquals("Forename", loadedUser.getForename());
assertEquals("Surname", loadedUser.getSurname());
}
}

3
settings.gradle

@ -14,7 +14,8 @@ def String[] samples = [ @@ -14,7 +14,8 @@ def String[] samples = [
'tutorial',
'contacts',
'openid',
'aspectj'
'aspectj',
'gae'
]
def String[] docs = [

4
web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
package org.springframework.security.web.authentication.preauth;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
@ -10,7 +9,6 @@ import javax.servlet.http.HttpServletRequest; @@ -10,7 +9,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.security.authentication.AuthenticationDetailsSource;
@ -55,7 +53,7 @@ import org.springframework.web.filter.GenericFilterBean; @@ -55,7 +53,7 @@ import org.springframework.web.filter.GenericFilterBean;
* @since 2.0
*/
public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFilterBean implements
InitializingBean, ApplicationEventPublisherAware {
ApplicationEventPublisherAware {
private ApplicationEventPublisher eventPublisher = null;
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();

Loading…
Cancel
Save