From ea3d35f43b469f02e2a7c05bfd99b9bf0ae5141b Mon Sep 17 00:00:00 2001 From: mitchellsundt Date: Thu, 23 Feb 2012 17:10:22 -0800 Subject: [PATCH] Initial check-in for 1.0.5 Release Candidate. --- bitrock-installer/buildWar.xml | 2 +- bitrock-installer/files/LICENSE.txt | 3 + eclipse-aggregate-gae/.classpath | 80 +++-- .../WEB-INF/applicationContext-security.xml | 146 ++++++-- .../WEB-INF/lib/jackson-core-asl-1.8.8.jar | 3 + .../WEB-INF/lib/jackson-mapper-asl-1.8.8.jar | 3 + eclipse-aggregate-gae/war/WEB-INF/web.xml | 27 +- eclipse-aggregate-gae/war/robots.txt | 2 + pom.xml | 15 + .../permissions/AccessConfigurationSheet.java | 83 ++++- .../AccessConfigurationSheet.ui.xml | 3 +- .../aggregate/constants/common/UIConsts.java | 2 +- .../servlet/OpenIdLoginPageServlet.java | 2 +- .../security/client/UserSecurityInfo.java | 12 +- .../security/common/GrantedAuthorityName.java | 2 + .../security/server/SecurityServiceImpl.java | 5 +- .../security/server/SecurityServiceUtil.java | 4 +- .../spring/BasicAuthenticationFilter.java | 47 +++ .../spring/DigestAuthenticationFilter.java | 46 +++ .../spring/Oauth2AuthenticationProvider.java | 179 ++++++++++ .../spring/Oauth2AuthenticationToken.java | 109 ++++++ .../security/spring/Oauth2ResourceFilter.java | 335 ++++++++++++++++++ .../spring/OpenIDAuthenticationFilter.java | 46 +++ .../OutOfBandAuthenticationProvider.java | 179 ++++++++++ .../spring/OutOfBandAuthenticationToken.java | 93 +++++ .../security/spring/OutOfBandUserFilter.java | 108 ++++++ ...ctingLoginUrlAuthenticationEntryPoint.java | 92 +++++ .../security/spring/RegisteredUsersTable.java | 18 +- ...uestAwareAuthenticationSuccessHandler.java | 77 ++++ .../spring/UserDetailsServiceImpl.java | 13 +- .../common/utils/OutOfBandUserFetcher.java | 39 ++ .../utils/gae/GaeOutOfBandUserFetcher.java | 54 +++ .../tomcat/TomcatOutOfBandUserFetcher.java | 33 ++ .../common/web/servlet/CommonServletBase.java | 40 +++ src/main/resources/gae-unit/odk-settings.xml | 1 + src/main/resources/gae/odk-settings.xml | 1 + .../resources/mysql-unit/odk-settings.xml | 1 + src/main/resources/mysql/odk-settings.xml | 1 + .../resources/postgres-unit/odk-settings.xml | 1 + src/main/resources/postgres/odk-settings.xml | 1 + 40 files changed, 1804 insertions(+), 104 deletions(-) create mode 100644 eclipse-aggregate-gae/war/WEB-INF/lib/jackson-core-asl-1.8.8.jar create mode 100644 eclipse-aggregate-gae/war/WEB-INF/lib/jackson-mapper-asl-1.8.8.jar create mode 100644 eclipse-aggregate-gae/war/robots.txt create mode 100644 src/main/java/org/opendatakit/common/security/spring/BasicAuthenticationFilter.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/DigestAuthenticationFilter.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationProvider.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationToken.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/Oauth2ResourceFilter.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/OpenIDAuthenticationFilter.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationProvider.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationToken.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/OutOfBandUserFilter.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/RedirectingLoginUrlAuthenticationEntryPoint.java create mode 100644 src/main/java/org/opendatakit/common/security/spring/TargetUrlRequestAwareAuthenticationSuccessHandler.java create mode 100644 src/main/java/org/opendatakit/common/utils/OutOfBandUserFetcher.java create mode 100644 src/main/java/org/opendatakit/common/utils/gae/GaeOutOfBandUserFetcher.java create mode 100644 src/main/java/org/opendatakit/common/utils/tomcat/TomcatOutOfBandUserFetcher.java diff --git a/bitrock-installer/buildWar.xml b/bitrock-installer/buildWar.xml index fc9a658477..8c267a30a4 100644 --- a/bitrock-installer/buildWar.xml +++ b/bitrock-installer/buildWar.xml @@ -1,7 +1,7 @@ ODKAggregate ODK Aggregate - 1.0.4 Production + 1.0.5 Release Candidate files/LICENSE.txt files/leftSide.png files/logo.png diff --git a/bitrock-installer/files/LICENSE.txt b/bitrock-installer/files/LICENSE.txt index 8b2e1d5f26..d79235a275 100644 --- a/bitrock-installer/files/LICENSE.txt +++ b/bitrock-installer/files/LICENSE.txt @@ -69,6 +69,9 @@ https://github.com/stain/jai-imageio-core /Modified BSD/ /Apache 2.0/ jai-imageio-core-standalone-1.2-pre-dr-b04-2010-04-30.jar Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. Modifications (c) Stian Soiland-Reyes 2010-04-30 +http://wiki.fasterxml.com/JacksonHome /Apache 2.0/ + jackson-core-asl-1.8.8.jar + jackson-mapper-asl-1.8.8.jar https://bitbucket.org/javarosa/javarosa/wiki/Home /Apache 2.0/ javarosa-libraries.jar http://code.google.com/p/atinject/ /Apache 2.0/ diff --git a/eclipse-aggregate-gae/.classpath b/eclipse-aggregate-gae/.classpath index 01a22620b0..967a431ef0 100644 --- a/eclipse-aggregate-gae/.classpath +++ b/eclipse-aggregate-gae/.classpath @@ -38,48 +38,50 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-aggregate-gae/war/WEB-INF/applicationContext-security.xml b/eclipse-aggregate-gae/war/WEB-INF/applicationContext-security.xml index 10b803de18..823ca4db3d 100644 --- a/eclipse-aggregate-gae/war/WEB-INF/applicationContext-security.xml +++ b/eclipse-aggregate-gae/war/WEB-INF/applicationContext-security.xml @@ -1,8 +1,10 @@ + xmlns:beans="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.1.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> @@ -63,6 +65,7 @@ + @@ -107,13 +110,15 @@ class="org.springframework.security.web.FilterChainProxy"> + filters="channelProcessingFilter,securityContextHolderAwareFilter,securityContextPersistenceFilter,logoutFilter,oauth2ResourceFilter,oobAuthFilter,${security.server.deviceAuthentication}AuthFilter,requestCacheAwareFilter,anonymousFilter,sessionManagerFilter,localExceptionTranslationFilter,filterSecurityInterceptor" /> + filters="channelProcessingFilter,securityContextHolderAwareFilter,securityContextPersistenceFilter,logoutFilter,oauth2ResourceFilter,oobAuthFilter,${security.server.deviceAuthentication}AuthFilter,openIdFilter,requestCacheAwareFilter,anonymousFilter,sessionManagerFilter,exceptionTranslationFilter,filterSecurityInterceptor" /> + filters="channelProcessingFilter,securityContextHolderAwareFilter,securityContextPersistenceFilter,logoutFilter,oauth2ResourceFilter,oobAuthFilter,${security.server.deviceAuthentication}AuthFilter,openIdFilter,requestCacheAwareFilter,anonymousFilter,sessionManagerFilter,exceptionTranslationFilter,filterSecurityInterceptor" /> + + @@ -227,17 +232,7 @@ - - - - - - - - - - + @@ -250,6 +245,16 @@ + + + + + + + + + @@ -272,7 +277,7 @@ + class="org.opendatakit.common.security.spring.BasicAuthenticationFilter"> @@ -321,7 +326,7 @@ --> + class="org.opendatakit.common.security.spring.DigestAuthenticationFilter"> @@ -361,14 +366,19 @@ - + + class="org.opendatakit.common.security.spring.OpenIDAuthenticationFilter"> spring-security-redirect + + + + + @@ -405,8 +415,19 @@ - + class="org.opendatakit.common.security.spring.RedirectingLoginUrlAuthenticationEntryPoint"> + + + + + + + + + + + + @@ -422,19 +443,76 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -474,6 +552,8 @@ + + diff --git a/eclipse-aggregate-gae/war/WEB-INF/lib/jackson-core-asl-1.8.8.jar b/eclipse-aggregate-gae/war/WEB-INF/lib/jackson-core-asl-1.8.8.jar new file mode 100644 index 0000000000..83896616a5 --- /dev/null +++ b/eclipse-aggregate-gae/war/WEB-INF/lib/jackson-core-asl-1.8.8.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96b394f135bf396679681aca6716d8bea14a97cf306d3738a053c43d07a1308b +size 227500 diff --git a/eclipse-aggregate-gae/war/WEB-INF/lib/jackson-mapper-asl-1.8.8.jar b/eclipse-aggregate-gae/war/WEB-INF/lib/jackson-mapper-asl-1.8.8.jar new file mode 100644 index 0000000000..4e497711fd --- /dev/null +++ b/eclipse-aggregate-gae/war/WEB-INF/lib/jackson-mapper-asl-1.8.8.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56436abd3e06c45e496b8604fd3f3b0f22451a9b5de8433b6f8b416e7a14a048 +size 668564 diff --git a/eclipse-aggregate-gae/war/WEB-INF/web.xml b/eclipse-aggregate-gae/war/WEB-INF/web.xml index e58f5e90de..0a60c7b72f 100644 --- a/eclipse-aggregate-gae/war/WEB-INF/web.xml +++ b/eclipse-aggregate-gae/war/WEB-INF/web.xml @@ -1,7 +1,12 @@ - + contextConfigLocation @@ -26,6 +31,10 @@ + + org.springframework.web.context.ContextLoaderListener + + serverSpringSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy @@ -36,13 +45,7 @@ /* - - org.springframework.web.context.ContextLoaderListener - - - - - + filterServiceImpl org.opendatakit.aggregate.server.FilterServiceImpl @@ -142,11 +145,11 @@ - oauth + oauthCallback org.opendatakit.aggregate.servlet.OAuthServlet - oauth + oauthCallback /auth/auth diff --git a/eclipse-aggregate-gae/war/robots.txt b/eclipse-aggregate-gae/war/robots.txt new file mode 100644 index 0000000000..c2aab7e039 --- /dev/null +++ b/eclipse-aggregate-gae/war/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/pom.xml b/pom.xml index 39be50f5c2..7fa83b9391 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ 1.6 2.3.2 4.10 + 1.8.8 3.1.0.RELEASE 1.2.1.1 1.6.4 @@ -197,6 +198,20 @@ + + org.codehaus.jackson + jackson-core-asl + ${org.codehaus.jackson.version} + jar + compile + + + org.codehaus.jackson + jackson-mapper-asl + ${org.codehaus.jackson.version} + jar + compile + com.google.appengine appengine-api-1.0-sdk diff --git a/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.java b/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.java index 6608ba926b..e353745fe9 100644 --- a/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.java +++ b/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.java @@ -122,7 +122,7 @@ private static final class AuthChangeValidation implements public boolean isValid(boolean prospectiveValue, UserSecurityInfo key) { // data collector must be an ODK account boolean badCollector = auth.equals(GrantedAuthorityName.GROUP_DATA_COLLECTORS) - && key.getUsername() == null; + && (key.getUsername() == null && !key.getEnableGoogleAuthTokens()); // site admin must not be the anonymous user boolean badSiteAdmin = auth.equals(GrantedAuthorityName.GROUP_SITE_ADMINS) && (key.getType() == UserType.ANONYMOUS); @@ -147,7 +147,9 @@ public boolean isVisible(UserSecurityInfo key) { if (auth == GrantedAuthorityName.GROUP_DATA_COLLECTORS) { // data collectors can only be ODK accounts... - return (key.getUsername() != null); + // or OpenId accounts that have tokens enabled... + return ((key.getUsername() != null) || + (key.getUsername() == null && key.getEnableGoogleAuthTokens())); } return true; } @@ -168,8 +170,12 @@ public boolean isEnabled(UserSecurityInfo info) { switch (auth) { case GROUP_DATA_COLLECTORS: - // data collectors must be anonymous or an ODK account type. - return (info.getType() == UserType.ANONYMOUS) || (info.getUsername() != null); + // data collectors must be anonymous + // or an ODK account type + // or have 'Enable Tokens' checked + return (info.getType() == UserType.ANONYMOUS) || + (info.getUsername() != null) || + ((info.getUsername() == null && info.getEnableGoogleAuthTokens())); case GROUP_DATA_VIEWERS: if (assignedGroups.contains(GrantedAuthorityName.GROUP_FORM_MANAGERS) || assignedGroups.contains(GrantedAuthorityName.GROUP_SITE_ADMINS)) { @@ -228,6 +234,58 @@ public int compare(UserSecurityInfo arg0, UserSecurityInfo arg1) { } } + private class EnableGoogleOauth2Column extends UIEnabledValidatingCheckboxColumn { + + protected EnableGoogleOauth2Column() { + super(new BooleanValidationPredicate() { + @Override + public boolean isValid(boolean prospectiveValue, UserSecurityInfo key) { + return (key.getUsername() == null); + }}, + new UIVisiblePredicate() { + @Override + public boolean isVisible(UserSecurityInfo key) { + return (key.getUsername() == null); + } + }, new EnableOpenIdAccountPredicate(), + new Comparator() { + + @Override + public int compare(UserSecurityInfo arg0, UserSecurityInfo arg1) { + boolean arg0Enabled = arg0.getEnableGoogleAuthTokens(); + boolean arg1Enabled = arg1.getEnableGoogleAuthTokens(); + + if (arg0Enabled == arg1Enabled) { + // same value. Order by whether or not we can enable + // this box (whether the user is an OpenId user). + arg0Enabled = (arg0.getUsername() == null); + arg1Enabled = (arg1.getUsername() == null); + if (arg0Enabled == arg1Enabled) + return 0; + if (arg0Enabled) + return -1; + return 1; + } + // checked before unchecked... + if (arg0Enabled) + return -1; + return 1; + }} ); + } + + @Override + public void setValue(UserSecurityInfo object, Boolean value) { + object.setEnableGoogleAuthTokens(value); + uiOutOfSyncWithServer(); + userTable.redraw(); // because this changes the Data Collector checkbox visibility... + } + + @Override + public Boolean getValue(UserSecurityInfo object) { + return object.getEnableGoogleAuthTokens(); + } + } + private class GroupMembershipColumn extends UIEnabledValidatingCheckboxColumn { final GrantedAuthorityName auth; @@ -346,8 +404,8 @@ public void updateUsersOnServer() { } break; } else { - if (i.getUsername() == null) { - // don't allow Google users to be data collectors + if (i.getUsername() == null && !i.getEnableGoogleAuthTokens()) { + // don't allow Google users without Tokens to be data collectors i.getAssignedUserGroups().remove(GrantedAuthorityName.GROUP_DATA_COLLECTORS); } } @@ -411,6 +469,14 @@ public boolean isEnabled(UserSecurityInfo info) { } }; + private static final class EnableOpenIdAccountPredicate implements + UIEnabledPredicate { + @Override + public boolean isEnabled(UserSecurityInfo info) { + return (info.getType() == UserType.REGISTERED && info.getUsername() == null); + } + }; + /** * Username cannot be null or zero-length. If it is a Google account type (an * e-mail address), then it should look like an e-mail address. @@ -684,6 +750,9 @@ public AccessConfigurationSheet(PermissionsSubTab permissionsTab) { "Change Password", new EnableLocalAccountPredicate(), new ChangePasswordActionCallback()); userTable.addColumn(changePassword, ""); + EnableGoogleOauth2Column ec = new EnableGoogleOauth2Column(); + userTable.addColumn( ec, "Enable Tokens"); + // Type of User AccountTypeSelectionColumn type = new AccountTypeSelectionColumn(); userTable.addColumn(type, "Account Type"); @@ -761,7 +830,7 @@ void onAddUsersClick(ClickEvent e) { ++nUnchanged; } else { u = new UserSecurityInfo(email.getUsername(), email.getFullName(), email.getEmail(), - UserType.REGISTERED); + UserType.REGISTERED, false); list.add(u); if (localUser) { localUsers.put(u.getUsername(), u); diff --git a/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.ui.xml b/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.ui.xml index 3de681782d..9bc00697b0 100644 --- a/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.ui.xml +++ b/src/main/java/org/opendatakit/aggregate/client/permissions/AccessConfigurationSheet.ui.xml @@ -7,10 +7,11 @@

Users access the site either

  • anonymously (i.e., as the anonymousUser), or
  • via a Google account, using OpenID (requires an Email account (e.g., user@gmail.com)), or
  • +
  • via an Oauth 1.0 or Oauth 2.0 token (check the Enable Tokens checkbox on a Google account), or
  • via an ODK account, with a username and password that a site administrator has configured for them.

Capabilities are as follows:

  • Data Collector - able to download forms to ODK Collect and submit data from ODK Collect to ODK Aggregate. -
    Only ODK accounts and the anonymousUser can be granted Data Collector rights. +
    Only ODK accounts, the anonymousUser and Oauth-enabled Google accounts can be granted Data Collector rights.
    The anonymousUser must be granted Data Collector rights to accept submissions from unidentified sources (e.g., from ODK Collect 1.1.5 and earlier, or from ODK Collect 1.1.7 and later if not authenticating).
  • Data Viewer - able to log onto the ODK Aggregate website, filter and view submissions, and generate diff --git a/src/main/java/org/opendatakit/aggregate/constants/common/UIConsts.java b/src/main/java/org/opendatakit/aggregate/constants/common/UIConsts.java index 53e78cd461..2f1da46dbc 100644 --- a/src/main/java/org/opendatakit/aggregate/constants/common/UIConsts.java +++ b/src/main/java/org/opendatakit/aggregate/constants/common/UIConsts.java @@ -17,7 +17,7 @@ package org.opendatakit.aggregate.constants.common; public class UIConsts { - public static final String VERSION_STRING = "v1.0.4 Production"; + public static final String VERSION_STRING = "v1.0.5 Release Candidate"; public static final String URI_DEFAULT = "no uuid"; public static final String FSC_URI_PARAM = "fsc"; diff --git a/src/main/java/org/opendatakit/aggregate/servlet/OpenIdLoginPageServlet.java b/src/main/java/org/opendatakit/aggregate/servlet/OpenIdLoginPageServlet.java index f6e0dcd73a..2f76b2c15b 100644 --- a/src/main/java/org/opendatakit/aggregate/servlet/OpenIdLoginPageServlet.java +++ b/src/main/java/org/opendatakit/aggregate/servlet/OpenIdLoginPageServlet.java @@ -152,7 +152,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se + "" + "" + " assignedUserGroups = new TreeSet(); TreeSet grantedAuthorities = new TreeSet(); public UserSecurityInfo() { } - public UserSecurityInfo(String username, String fullname, String email, UserType type) { + public UserSecurityInfo(String username, String fullname, String email, UserType type, boolean enableGoogleAuthTokens) { this.username = username; this.fullname = fullname; this.email = email; this.type = type; + this.enableGoogleAuthTokens = enableGoogleAuthTokens; if ( (email != null && username != null) || (email == null && username == null) ) { throw new IllegalArgumentException("must have either just username or just email non-null"); } } + public boolean getEnableGoogleAuthTokens() { + return enableGoogleAuthTokens; + } + + public void setEnableGoogleAuthTokens(boolean enableGoogleAuthTokens) { + this.enableGoogleAuthTokens = enableGoogleAuthTokens; + } + public UserType getType() { return type; } diff --git a/src/main/java/org/opendatakit/common/security/common/GrantedAuthorityName.java b/src/main/java/org/opendatakit/common/security/common/GrantedAuthorityName.java index 530256368c..6ce5469c7c 100644 --- a/src/main/java/org/opendatakit/common/security/common/GrantedAuthorityName.java +++ b/src/main/java/org/opendatakit/common/security/common/GrantedAuthorityName.java @@ -32,7 +32,9 @@ public enum GrantedAuthorityName implements IsSerializable { AUTH_LOCAL("any users authenticated via the locally-held (Aggregate password) credential"), + AUTH_OUT_OF_BAND("any users authenticated vio Out-of-band mechanisms"), AUTH_OPENID("any users authenticated via OpenID"), + AUTH_GOOGLE_OAUTH2("any users authenticated via Google Oauth2 proxy"), USER_IS_ANONYMOUS("for unauthenticated access"), USER_IS_REGISTERED("for registered users of this system (a user identified " + diff --git a/src/main/java/org/opendatakit/common/security/server/SecurityServiceImpl.java b/src/main/java/org/opendatakit/common/security/server/SecurityServiceImpl.java index 0010021364..31722f5104 100644 --- a/src/main/java/org/opendatakit/common/security/server/SecurityServiceImpl.java +++ b/src/main/java/org/opendatakit/common/security/server/SecurityServiceImpl.java @@ -70,14 +70,15 @@ public UserSecurityInfo getUserInfo() throws AccessDeniedException, t = RegisteredUsersTable.getUserByUri(uriUser, ds, user); if ( t != null ) { info = new UserSecurityInfo(t.getUsername(), t.getFullName(), t.getEmail(), - UserSecurityInfo.UserType.REGISTERED); + UserSecurityInfo.UserType.REGISTERED, + t.getGoogleTokenEnabled()); SecurityServiceUtil.setAuthenticationLists(info, t.getUri(), cc); } else { throw new DatastoreFailureException("Unable to retrieve user record"); } } else if ( user.isAnonymous() ) { info = new UserSecurityInfo(User.ANONYMOUS_USER, User.ANONYMOUS_USER_NICKNAME, null, - UserSecurityInfo.UserType.ANONYMOUS); + UserSecurityInfo.UserType.ANONYMOUS, false); SecurityServiceUtil.setAuthenticationListsForSpecialUser(info, GrantedAuthorityName.USER_IS_ANONYMOUS, cc); } else { // should never get to this case via interactive actions... diff --git a/src/main/java/org/opendatakit/common/security/server/SecurityServiceUtil.java b/src/main/java/org/opendatakit/common/security/server/SecurityServiceUtil.java index d1a961ff03..8b93a12e68 100644 --- a/src/main/java/org/opendatakit/common/security/server/SecurityServiceUtil.java +++ b/src/main/java/org/opendatakit/common/security/server/SecurityServiceUtil.java @@ -127,14 +127,14 @@ public static ArrayList getAllUsers(boolean withAuthorities, C for ( CommonFieldsBase cb : l ) { RegisteredUsersTable t = (RegisteredUsersTable) cb; UserSecurityInfo i = new UserSecurityInfo(t.getUsername(), t.getFullName(), t.getEmail(), - UserSecurityInfo.UserType.REGISTERED); + UserSecurityInfo.UserType.REGISTERED, t.getGoogleTokenEnabled()); if ( withAuthorities ) { SecurityServiceUtil.setAuthenticationLists(i, t.getUri(), cc); } users.add(i); } // TODO: why doesn't this work? - UserSecurityInfo anonymous = new UserSecurityInfo(User.ANONYMOUS_USER, User.ANONYMOUS_USER_NICKNAME, null, UserSecurityInfo.UserType.ANONYMOUS); + UserSecurityInfo anonymous = new UserSecurityInfo(User.ANONYMOUS_USER, User.ANONYMOUS_USER_NICKNAME, null, UserSecurityInfo.UserType.ANONYMOUS, false); if ( withAuthorities ) { SecurityServiceUtil.setAuthenticationListsForSpecialUser(anonymous, GrantedAuthorityName.USER_IS_ANONYMOUS, cc); } diff --git a/src/main/java/org/opendatakit/common/security/spring/BasicAuthenticationFilter.java b/src/main/java/org/opendatakit/common/security/spring/BasicAuthenticationFilter.java new file mode 100644 index 0000000000..77d93e5c85 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/BasicAuthenticationFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Wraps the Spring class and ensures that if an Authentication is already + * determined for this request, that it isn't overridden. + * + * @author mitchellsundt@gmail.com + * + */ +@SuppressWarnings("deprecation") +public class BasicAuthenticationFilter extends org.springframework.security.web.authentication.www.BasicAuthenticationFilter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if ( SecurityContextHolder.getContext().getAuthentication() == null ) { + super.doFilter(request, response, chain); + } else { + chain.doFilter(request, response); + } + } + +} diff --git a/src/main/java/org/opendatakit/common/security/spring/DigestAuthenticationFilter.java b/src/main/java/org/opendatakit/common/security/spring/DigestAuthenticationFilter.java new file mode 100644 index 0000000000..2132c50e26 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/DigestAuthenticationFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Wraps the Spring class and ensures that if an Authentication is already + * determined for this request, that it isn't overridden. + * + * @author mitchellsundt@gmail.com + * + */ +public class DigestAuthenticationFilter extends org.springframework.security.web.authentication.www.DigestAuthenticationFilter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if ( SecurityContextHolder.getContext().getAuthentication() == null ) { + super.doFilter(request, response, chain); + } else { + chain.doFilter(request, response); + } + } + +} diff --git a/src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationProvider.java b/src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationProvider.java new file mode 100644 index 0000000000..7345807520 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationProvider.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2012 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.opendatakit.common.security.SecurityUtils; +import org.opendatakit.common.security.common.GrantedAuthorityName; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/** + * Repackaged WrappingOpenIDAuthenticationProvider that attaches the + * AUTH_GOOGLE_OAUTH2 grant to the recognized user. + * + * @author mitchellsundt@gmail.com + * + */ +public class Oauth2AuthenticationProvider implements AuthenticationProvider, InitializingBean { + + private static final Log logger = LogFactory.getLog(Oauth2AuthenticationProvider.class); + + //~ Instance fields ================================================================================================ + + UserDetailsService wrappingUserDetailsService; + + public UserDetailsService getWrappingUserDetailsService() { + return wrappingUserDetailsService; + } + + public void setWrappingUserDetailsService( + UserDetailsService wrappingUserDetailsService) { + this.wrappingUserDetailsService = wrappingUserDetailsService; + } + + public void afterPropertiesSet() throws Exception { + if ( wrappingUserDetailsService == null ) { + throw new IllegalStateException("wrappingUserDetailsService must be defined"); + } + } + + /* (non-Javadoc) + * @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class) + */ + public boolean supports(Class authentication) { + return Oauth2AuthenticationToken.class.isAssignableFrom(authentication); + } + + /* (non-Javadoc) + * @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.Authentication) + */ + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { + + if (!supports(authentication.getClass())) { + return null; + } + + if (authentication instanceof Oauth2AuthenticationToken) { + Oauth2AuthenticationToken response = (Oauth2AuthenticationToken) authentication; + // Lookup user details + UserDetails userDetails = + new User(response.getName(), UUID.randomUUID().toString(), + true, true, true, true, new ArrayList() ); + return createSuccessfulAuthentication(userDetails, response); + } + + return null; + } + + /** + * Handles the creation of the final Authentication object which will be returned by the provider. + *

    + * The default implementation just creates a new OutOfBandAuthenticationToken from the original, but with the + * UserDetails as the principal and including the authorities loaded by the UserDetailsService. + * + * @param userDetails the loaded UserDetails object + * @param auth the token passed to the authenticate method, containing + * @return the token which will represent the authenticated user. + */ + protected Authentication createSuccessfulAuthentication( + UserDetails rawUserDetails, Oauth2AuthenticationToken auth) { + String eMail = auth.getEmail(); + if ( eMail == null ) { + logger.warn("OpenId attributes did not include an e-mail address! "); + throw new UsernameNotFoundException("email address not supplied in OpenID attributes"); + } + eMail = Oauth2AuthenticationProvider.normalizeMailtoAddress(eMail); + String mailtoDomain = Oauth2AuthenticationProvider.getMailtoDomain(eMail); + + UserDetails userDetails = rawUserDetails; + + Set authorities = new HashSet(); + + authorities.addAll(userDetails.getAuthorities()); + // add the AUTH_OPENID granted authority, + authorities.add(new SimpleGrantedAuthority(GrantedAuthorityName.AUTH_GOOGLE_OAUTH2.toString())); + + // attempt to look user up in registered users table... + String username = null; + UserDetails partialDetails = null; + boolean noRights = false; + try { + partialDetails = wrappingUserDetailsService.loadUserByUsername(eMail); + // found the user in the table -- fold in authorizations and get uriUser. + authorities.addAll(partialDetails.getAuthorities()); + // users are blacklisted by registering them and giving them no rights. + noRights = partialDetails.getAuthorities().isEmpty(); + username = partialDetails.getUsername(); + } catch (Exception e) { + logger.warn("OpenId attribute e-mail: " + eMail + " did not match any known e-mail addresses! " + e.getMessage()); + throw new UsernameNotFoundException("account not recognized"); + } + + AggregateUser trueUser = new AggregateUser(username, + partialDetails.getPassword(), + UUID.randomUUID().toString(), // junk... + mailtoDomain, + partialDetails.isEnabled(), + partialDetails.isAccountNonExpired(), + partialDetails.isCredentialsNonExpired(), + partialDetails.isAccountNonLocked(), + authorities); + if ( noRights || !( trueUser.isEnabled() && trueUser.isAccountNonExpired() && + trueUser.isAccountNonLocked() ) ) { + logger.warn("OpenId attribute e-mail: " + eMail + " account is blocked! "); + throw new UsernameNotFoundException("account is blocked"); + } + + return new Oauth2AuthenticationToken(trueUser, trueUser.getAuthorities(), + auth.getAccessToken(), auth.getEmail(), auth.getExpiration()); + } + + public static final String getMailtoDomain( String uriUser ) { + if ( uriUser == null || + !uriUser.startsWith(SecurityUtils.MAILTO_COLON) || + !uriUser.contains(SecurityUtils.AT_SIGN) ) + return null; + return uriUser.substring(uriUser.indexOf(SecurityUtils.AT_SIGN)+1); + } + + public static final String normalizeMailtoAddress(String emailAddress) { + String mailtoUsername = emailAddress; + if ( !emailAddress.startsWith(SecurityUtils.MAILTO_COLON) ) { + if ( emailAddress.contains(SecurityUtils.AT_SIGN) ) { + mailtoUsername = SecurityUtils.MAILTO_COLON + emailAddress; + } else { + logger.warn("OpenId attribute e-mail: " + emailAddress + " does not specify a domain! "); + throw new IllegalStateException("e-mail address is incomplete - it does not specify a domain!"); + } + } + return mailtoUsername; + } +} diff --git a/src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationToken.java b/src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationToken.java new file mode 100644 index 0000000000..0045ca926d --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/Oauth2AuthenticationToken.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; + +import org.opendatakit.common.utils.WebUtils; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +/** + * Authentication that is returned by Oauth 2.0 processing. + * + * @author mitchellsundt@gmail.com + * + */ +public class Oauth2AuthenticationToken extends AbstractAuthenticationToken { + + /** + * + */ + private static final long serialVersionUID = 957149283924292479L; + + //~ Instance fields ================================================================================================ + private final Object principal; + private String accessToken; + private String email; + private Date expiration; + + public Oauth2AuthenticationToken(String accessToken, String email, Date expiration) { + super(new ArrayList(0)); + this.principal = accessToken; + this.accessToken = accessToken; + this.email = email; + this.expiration = expiration; + + setAuthenticated(false); + } + + /** + * Created by the Oauth2AuthenticationProvider on successful authentication. + * + * @param principal usually the UserDetails returned by the the configured UserDetailsService + * used by the Oauth2AuthenticationProvider. + * + */ + public Oauth2AuthenticationToken(Object principal, Collection authorities, + String accessToken, String email, Date expiration) { + super(authorities); + this.principal = principal; + this.accessToken = accessToken; + this.email = email; + this.expiration = expiration; + + setAuthenticated(true); + } + + //~ Methods ======================================================================================================== + + /** + * Returns 'null' always, as no credentials are processed by the OpenID provider. + * @see org.springframework.security.core.Authentication#getCredentials() + */ + public Object getCredentials() { + return null; + } + + public String getAccessToken() { + return accessToken; + } + + public String getEmail() { + return email; + } + + public Date getExpiration() { + return expiration; + } + + /** + * Returns the principal value. + * + * @see org.springframework.security.core.Authentication#getPrincipal() + */ + public Object getPrincipal() { + return principal; + } + + @Override + public String toString() { + return "[" + super.toString() + ", email : " + email + + ", expiration : " + WebUtils.iso8601Date(expiration) + "]"; + } +} diff --git a/src/main/java/org/opendatakit/common/security/spring/Oauth2ResourceFilter.java b/src/main/java/org/opendatakit/common/security/spring/Oauth2ResourceFilter.java new file mode 100644 index 0000000000..72e3655fd3 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/Oauth2ResourceFilter.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2012 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.params.ClientPNames; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.client.utils.URIUtils; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.BasicHttpContext; +import org.codehaus.jackson.map.ObjectMapper; +import org.opendatakit.common.utils.HttpClientFactory; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +/** + * If the session does not already have an Authentication element, + * this filter looks for an Oauth 2.0 access token that gives access + * to a user's Google E-mail address (i.e., has a userinfo.email scope). + * This is considered sufficient to assume the user's identity provided + * the 'Enable Tokens' checkbox is checked. + * + * This e-mail needs to be in the registered + * users table and assigned permissions. + * + * Ideally, we would have a custom scope in the Google Oauth 2.0 grant + * for this Aggregate instance. That is not yet possible. + * + * @author mitchellsundt@gmail.com + * + */ +public class Oauth2ResourceFilter extends GenericFilterBean { + + private static final int SERVICE_TIMEOUT_MILLISECONDS = 30000; + private static final int SOCKET_ESTABLISHMENT_TIMEOUT_MILLISECONDS = 60000; + private static final String ACCESS_TOKEN = "access_token"; + private static final String BEARER_TYPE = "Bearer"; + + private Log logger = LogFactory.getLog(Oauth2ResourceFilter.class); + private static final ObjectMapper mapper = new ObjectMapper(); + + private AuthenticationProvider authenticationProvider = null; + private HttpClientFactory httpClientFactory = null; + + private static Map authTokenMap + = new HashMap(); + + private static synchronized Oauth2AuthenticationToken lookupToken(String accessToken) { + + Oauth2AuthenticationToken ti = authTokenMap.get(accessToken); + if ( ti != null && ( ti.getExpiration().compareTo(new Date()) < 0 )) { + // expired... + authTokenMap.remove(ti); + ti = null; + } + + return ti; + } + + private static synchronized void insertToken(Oauth2AuthenticationToken token) { + authTokenMap.put(token.getAccessToken(), token); + } + + public Oauth2ResourceFilter() { + super(); + } + + public AuthenticationProvider getAuthenticationProvider() { + return authenticationProvider; + } + + public void setAuthenticationProvider(AuthenticationProvider authenticationProvider) { + this.authenticationProvider = authenticationProvider; + } + + public HttpClientFactory getHttpClientFactory() { + return httpClientFactory; + } + + public void setHttpClientFactory(HttpClientFactory factory) { + httpClientFactory = factory; + } + + @Override + public void afterPropertiesSet() throws ServletException { + super.afterPropertiesSet(); + if ( httpClientFactory == null ) { + throw new IllegalStateException("httpClientFactory must be defined"); + } + if ( authenticationProvider == null ) { + throw new IllegalStateException("authenticationProvider must be defined"); + } + } + + protected String parseToken(HttpServletRequest request) { + // first check the header... + String token = parseHeaderToken(request); + + // bearer type allows a request parameter as well + if (token == null) { + logger.debug("Token not found in headers. Trying request parameters."); + token = request.getParameter(ACCESS_TOKEN); + if (token == null) { + logger.debug("Token not found in request parameters. Not an OAuth2 request."); + } + } + + return token; + } + + /** + * Parse the OAuth header parameters. The parameters will be oauth-decoded. + * + * @param request The request. + * @return The parsed parameters, or null if no OAuth authorization header was supplied. + */ + protected String parseHeaderToken(HttpServletRequest request) { + @SuppressWarnings("unchecked") + Enumeration headers = request.getHeaders("Authorization"); + while (headers.hasMoreElements()) { + String value = headers.nextElement(); + if ((value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase()))) { + String authHeaderValue = value.substring(BEARER_TYPE.length()).trim(); + + if (authHeaderValue.contains("oauth_signature_method") || authHeaderValue.contains("oauth_verifier")) { + // presence of oauth_signature_method or oauth_verifier implies an oauth 1.x request + continue; + } + + int commaIndex = authHeaderValue.indexOf(','); + if (commaIndex > 0) { + authHeaderValue = authHeaderValue.substring(0, commaIndex); + } + + // todo: parse any parameters... + + return authHeaderValue; + } + else { + // todo: support additional authorization schemes for different token types, e.g. "MAC" specified by + // http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token + } + } + + return null; + } + + private Map getJsonResponse(String url, String accessToken) { + + Map nullData = new HashMap(); + + // OK if we got here, we have a valid token. + // Issue the request... + URI nakedUri; + try { + nakedUri = new URI(url); + } catch (URISyntaxException e2) { + e2.printStackTrace(); + logger.error(e2.toString()); + return nullData; + } + + List qparams = new ArrayList(); + qparams.add(new BasicNameValuePair("access_token", accessToken)); + URI uri; + try { + uri = URIUtils.createURI(nakedUri.getScheme(), nakedUri.getHost(), nakedUri.getPort(), nakedUri.getPath(), + URLEncodedUtils.format(qparams, "UTF-8"), null); + } catch (URISyntaxException e1) { + e1.printStackTrace(); + logger.error(e1.toString()); + return nullData; + } + + // DON'T NEED clientId on the toke request... + // addCredentials(clientId, clientSecret, nakedUri.getHost()); + // setup request interceptor to do preemptive auth + // ((DefaultHttpClient) client).addRequestInterceptor(getPreemptiveAuth(), 0); + + HttpParams httpParams = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParams, SERVICE_TIMEOUT_MILLISECONDS); + HttpConnectionParams.setSoTimeout(httpParams, SOCKET_ESTABLISHMENT_TIMEOUT_MILLISECONDS); + // support redirecting to handle http: => https: transition + HttpClientParams.setRedirecting(httpParams, true); + // support authenticating + HttpClientParams.setAuthenticating(httpParams, true); + + httpParams.setParameter(ClientPNames.MAX_REDIRECTS, 1); + httpParams.setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true); + // setup client + HttpClient client = httpClientFactory.createHttpClient(httpParams); + + HttpGet httpget = new HttpGet(uri); + logger.info(httpget.getURI().toString()); + + HttpResponse response = null; + try { + response = client.execute(httpget, new BasicHttpContext()); + int statusCode = response.getStatusLine().getStatusCode(); + + if ( statusCode != HttpStatus.SC_OK ) { + logger.error("not 200: " + statusCode); + return nullData; + } else { + HttpEntity entity = response.getEntity(); + + if (entity != null && entity.getContentType().getValue().toLowerCase() + .contains("json")) { + BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent())); + @SuppressWarnings("unchecked") + Map userData = mapper.readValue(reader, Map.class); + + return userData; + } else { + logger.error("unexpected body"); + return nullData; + } + } + } catch ( IOException e ) { + logger.error(e.toString()); + return nullData; + } + } + + private Oauth2AuthenticationToken assertToken(String accessToken) { + + Oauth2AuthenticationToken ti = lookupToken(accessToken); + if ( ti != null ) { + return ti; + } + + Map responseData; + + responseData = getJsonResponse("https://www.googleapis.com/oauth2/v1/tokeninfo", accessToken); + + Integer expiresInSeconds = (Integer) responseData.get("expires_in"); + if ( expiresInSeconds == null || expiresInSeconds == 0 ) { + return null; + } + Date deadline = null; + deadline = new Date( System.currentTimeMillis() + 1000L*expiresInSeconds); + + String email = (String) responseData.get("email"); + if ( email == null ) { + responseData = getJsonResponse("https://www.googleapis.com/oauth2/v1/userinfo", accessToken); + + email = (String) responseData.get("email"); + if ( email == null ) { + return null; + } + } + + ti = new Oauth2AuthenticationToken( accessToken, email, deadline); + insertToken(ti); + return ti; + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + if ( SecurityContextHolder.getContext().getAuthentication() == null ) { + try { + String accessToken = parseToken(request); + + if (accessToken != null) { + Oauth2AuthenticationToken token = assertToken(accessToken); + + if ( token != null ) { + // In the common case, if token is non-null, the user should be known to the server. + Authentication auth = + authenticationProvider.authenticate(token); + + SecurityContextHolder.getContext().setAuthentication(auth); + } + } + } catch ( AuthenticationException ex ) { + // if the authentication fails to recognize the user, silently ignore the failure. + // Warnings were already logged by the AuthenticationProvider. + } + } + + chain.doFilter(request, response); + } + +} diff --git a/src/main/java/org/opendatakit/common/security/spring/OpenIDAuthenticationFilter.java b/src/main/java/org/opendatakit/common/security/spring/OpenIDAuthenticationFilter.java new file mode 100644 index 0000000000..7012e2a8cf --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/OpenIDAuthenticationFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Wraps the Spring class and ensures that if an Authentication is already + * determined for this request, that it isn't overridden. + * + * @author mitchellsundt@gmail.com + * + */ +public class OpenIDAuthenticationFilter extends org.springframework.security.openid.OpenIDAuthenticationFilter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if ( SecurityContextHolder.getContext().getAuthentication() == null ) { + super.doFilter(request, response, chain); + } else { + chain.doFilter(request, response); + } + } + +} diff --git a/src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationProvider.java b/src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationProvider.java new file mode 100644 index 0000000000..1f98a99a92 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationProvider.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2012 University of Washington + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.opendatakit.common.security.SecurityUtils; +import org.opendatakit.common.security.common.GrantedAuthorityName; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/** + * Repackaged WrappingOpenIDAuthenticationProvider that attaches the + * AUTH_OUT_OF_BAND grant to the recognized user. + * + * @author mitchellsundt@gmail.com + * + */ +public class OutOfBandAuthenticationProvider implements AuthenticationProvider, InitializingBean { + + private static final Log logger = LogFactory.getLog(OutOfBandAuthenticationProvider.class); + + //~ Instance fields ================================================================================================ + + UserDetailsService wrappingUserDetailsService; + + public UserDetailsService getWrappingUserDetailsService() { + return wrappingUserDetailsService; + } + + public void setWrappingUserDetailsService( + UserDetailsService wrappingUserDetailsService) { + this.wrappingUserDetailsService = wrappingUserDetailsService; + } + + public void afterPropertiesSet() throws Exception { + if ( wrappingUserDetailsService == null ) { + throw new IllegalStateException("wrappingUserDetailsService must be defined"); + } + } + + /* (non-Javadoc) + * @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class) + */ + public boolean supports(Class authentication) { + return OutOfBandAuthenticationToken.class.isAssignableFrom(authentication); + } + + /* (non-Javadoc) + * @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.Authentication) + */ + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { + + if (!supports(authentication.getClass())) { + return null; + } + + if (authentication instanceof OutOfBandAuthenticationToken) { + OutOfBandAuthenticationToken response = (OutOfBandAuthenticationToken) authentication; + // Lookup user details + UserDetails userDetails = + new User(response.getName(), UUID.randomUUID().toString(), + true, true, true, true, new ArrayList() ); + return createSuccessfulAuthentication(userDetails, response); + } + + return null; + } + + /** + * Handles the creation of the final Authentication object which will be returned by the provider. + *

    + * The default implementation just creates a new OutOfBandAuthenticationToken from the original, but with the + * UserDetails as the principal and including the authorities loaded by the UserDetailsService. + * + * @param userDetails the loaded UserDetails object + * @param auth the token passed to the authenticate method, containing + * @return the token which will represent the authenticated user. + */ + protected Authentication createSuccessfulAuthentication( + UserDetails rawUserDetails, OutOfBandAuthenticationToken auth) { + String eMail = auth.getEmail(); + if ( eMail == null ) { + logger.warn("OpenId attributes did not include an e-mail address! "); + throw new UsernameNotFoundException("email address not supplied in OpenID attributes"); + } + eMail = OutOfBandAuthenticationProvider.normalizeMailtoAddress(eMail); + String mailtoDomain = OutOfBandAuthenticationProvider.getMailtoDomain(eMail); + + UserDetails userDetails = rawUserDetails; + + Set authorities = new HashSet(); + + authorities.addAll(userDetails.getAuthorities()); + // add the AUTH_OPENID granted authority, + authorities.add(new SimpleGrantedAuthority(GrantedAuthorityName.AUTH_OUT_OF_BAND.toString())); + + // attempt to look user up in registered users table... + String username = null; + UserDetails partialDetails = null; + boolean noRights = false; + try { + partialDetails = wrappingUserDetailsService.loadUserByUsername(eMail); + // found the user in the table -- fold in authorizations and get uriUser. + authorities.addAll(partialDetails.getAuthorities()); + // users are blacklisted by registering them and giving them no rights. + noRights = partialDetails.getAuthorities().isEmpty(); + username = partialDetails.getUsername(); + } catch (Exception e) { + logger.warn("OpenId attribute e-mail: " + eMail + " did not match any known e-mail addresses! " + e.getMessage()); + throw new UsernameNotFoundException("account not recognized"); + } + + AggregateUser trueUser = new AggregateUser(username, + partialDetails.getPassword(), + UUID.randomUUID().toString(), // junk... + mailtoDomain, + partialDetails.isEnabled(), + partialDetails.isAccountNonExpired(), + partialDetails.isCredentialsNonExpired(), + partialDetails.isAccountNonLocked(), + authorities); + if ( noRights || !( trueUser.isEnabled() && trueUser.isAccountNonExpired() && + trueUser.isAccountNonLocked() ) ) { + logger.warn("OpenId attribute e-mail: " + eMail + " account is blocked! "); + throw new UsernameNotFoundException("account is blocked"); + } + + return new OutOfBandAuthenticationToken(trueUser, trueUser.getAuthorities(), + auth.getEmail()); + } + + public static final String getMailtoDomain( String uriUser ) { + if ( uriUser == null || + !uriUser.startsWith(SecurityUtils.MAILTO_COLON) || + !uriUser.contains(SecurityUtils.AT_SIGN) ) + return null; + return uriUser.substring(uriUser.indexOf(SecurityUtils.AT_SIGN)+1); + } + + public static final String normalizeMailtoAddress(String emailAddress) { + String mailtoUsername = emailAddress; + if ( !emailAddress.startsWith(SecurityUtils.MAILTO_COLON) ) { + if ( emailAddress.contains(SecurityUtils.AT_SIGN) ) { + mailtoUsername = SecurityUtils.MAILTO_COLON + emailAddress; + } else { + logger.warn("OpenId attribute e-mail: " + emailAddress + " does not specify a domain! "); + throw new IllegalStateException("e-mail address is incomplete - it does not specify a domain!"); + } + } + return mailtoUsername; + } +} diff --git a/src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationToken.java b/src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationToken.java new file mode 100644 index 0000000000..38babe7588 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/OutOfBandAuthenticationToken.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2012 University of Washington. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; + +/** + * Authentication token returned when an out-of-band user identity is processed. + * These are considered token-based authentications. + * + * @author mitchellsundt@gmail.com + * + */ +public class OutOfBandAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + //~ Instance fields ================================================================================================ + private final Object principal; + private final String email; + + //~ Constructors =================================================================================================== + + public OutOfBandAuthenticationToken(String email) { + super(new ArrayList(0)); + this.principal = email; + this.email = email; + + setAuthenticated(false); + } + + /** + * Created by the OpenIDAuthenticationProvider on successful authentication. + * + * @param principal usually the UserDetails returned by the the configured UserDetailsService + * used by the OpenIDAuthenticationProvider. + * + */ + public OutOfBandAuthenticationToken(Object principal, Collection authorities, + String email) { + super(authorities); + this.principal = principal; + this.email = email; + + setAuthenticated(true); + } + + //~ Methods ======================================================================================================== + + /** + * Returns 'null' always, as no credentials are processed by the OpenID provider. + * @see org.springframework.security.core.Authentication#getCredentials() + */ + public Object getCredentials() { + return null; + } + + public String getEmail() { + return email; + } + + /** + * Returns the principal value. + * + * @see org.springframework.security.core.Authentication#getPrincipal() + */ + public Object getPrincipal() { + return principal; + } + + @Override + public String toString() { + return "[" + super.toString() + ", email : " + email +"]"; + } +} diff --git a/src/main/java/org/opendatakit/common/security/spring/OutOfBandUserFilter.java b/src/main/java/org/opendatakit/common/security/spring/OutOfBandUserFilter.java new file mode 100644 index 0000000000..8e67572848 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/OutOfBandUserFilter.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2012 University of Washington. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.opendatakit.common.utils.OutOfBandUserFetcher; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; +import org.springframework.web.filter.GenericFilterBean; + +/** + * If the session does not already have an Authentication element, + * this filter attempts to find an out-of-band (e.g., Google AppEngine + * OAuthService) fetcher to return the user e-mail to use for + * authorization decisions. That e-mail needs to be in the registered + * users table and assigned permissions on the website. + * + * Any user returned by the outOfBandUserFetcher is assumed to have been + * properly authenticated. This mechanism could be used to obtain user + * identities authenticated through a portal. + * + * @author mitchellsundt@gmail.com + * + */ +public class OutOfBandUserFilter extends GenericFilterBean { + + Log logger = LogFactory.getLog(OutOfBandUserFilter.class); + + AuthenticationProvider authenticationProvider = null; + OutOfBandUserFetcher outOfBandUserFetcher = null; + + public AuthenticationProvider getAuthenticationProvider() { + return authenticationProvider; + } + + public void setAuthenticationProvider(AuthenticationProvider authenticationProvider) { + this.authenticationProvider = authenticationProvider; + } + + public OutOfBandUserFetcher getOutOfBandUserFetcher() { + return outOfBandUserFetcher; + } + + public void setOutOfBandUserFetcher(OutOfBandUserFetcher outOfBandUserFetcher) { + this.outOfBandUserFetcher = outOfBandUserFetcher; + } + + @Override + public void afterPropertiesSet() throws ServletException { + super.afterPropertiesSet(); + Assert.notNull(outOfBandUserFetcher, "OutOfBandUserFetcher must be supplied."); + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + if ( SecurityContextHolder.getContext().getAuthentication() == null ) { + try { + String userEmail = outOfBandUserFetcher.getEmail(); + + if (userEmail != null) { + + // In the common case, if userEmail is non-null, the user should be known to the server. + Authentication auth = + authenticationProvider.authenticate(new OutOfBandAuthenticationToken(userEmail)); + + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch ( AuthenticationException ex ) { + // if the authentication fails to recognize the user, silently ignore the failure. + // Warnings were already logged by the AuthenticationProvider. + } + } + + chain.doFilter(request, response); + } + +} diff --git a/src/main/java/org/opendatakit/common/security/spring/RedirectingLoginUrlAuthenticationEntryPoint.java b/src/main/java/org/opendatakit/common/security/spring/RedirectingLoginUrlAuthenticationEntryPoint.java new file mode 100644 index 0000000000..e36c3fdd71 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/RedirectingLoginUrlAuthenticationEntryPoint.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 University of Washington. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.RedirectUrlBuilder; +import org.springframework.security.web.util.UrlUtils; + +/** + * Respects the ?redirect=URL string during login and redirects to it + * upon successful login. + * + * @author mitchellsundt@gmail.com + * + */ +public class RedirectingLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint { + + private Log logger = LogFactory.getLog(RedirectingLoginUrlAuthenticationEntryPoint.class); + + public RedirectingLoginUrlAuthenticationEntryPoint(String loginFormUrl) { + super(loginFormUrl); + } + + + protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + + String loginForm = determineUrlToUseForThisRequest(request, response, authException); + + if (UrlUtils.isAbsoluteUrl(loginForm)) { + return loginForm; + } + + int serverPort = getPortResolver().getServerPort(request); + String scheme = request.getScheme(); + + RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); + + urlBuilder.setScheme(scheme); + urlBuilder.setServerName(request.getServerName()); + urlBuilder.setPort(serverPort); + urlBuilder.setContextPath(request.getContextPath()); + urlBuilder.setPathInfo(loginForm); + try { + String fullRequest = request.getRequestURL().toString(); + if ( request.getQueryString() != null ) { + fullRequest += "?" + request.getQueryString(); + } + String redirectParam = "redirect=" + URLEncoder.encode(fullRequest,"UTF-8"); + urlBuilder.setQuery(redirectParam); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + if (isForceHttps() && "http".equals(scheme)) { + Integer httpsPort = getPortMapper().lookupHttpsPort(Integer.valueOf(serverPort)); + + if (httpsPort != null) { + // Overwrite scheme and port in the redirect URL + urlBuilder.setScheme("https"); + urlBuilder.setPort(httpsPort.intValue()); + } else { + logger.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port " + serverPort); + } + } + + return urlBuilder.getUrl(); + } + +} diff --git a/src/main/java/org/opendatakit/common/security/spring/RegisteredUsersTable.java b/src/main/java/org/opendatakit/common/security/spring/RegisteredUsersTable.java index 7ac931700e..5dcbf49edc 100644 --- a/src/main/java/org/opendatakit/common/security/spring/RegisteredUsersTable.java +++ b/src/main/java/org/opendatakit/common/security/spring/RegisteredUsersTable.java @@ -101,6 +101,9 @@ public final class RegisteredUsersTable extends CommonFieldsBase { private static final DataField IS_REMOVED = new DataField( "IS_REMOVED", DataField.DataType.BOOLEAN, false ); + + private static final DataField GOOGLE_TOKEN_ENABLED = new DataField( + "GOOGLE_TOKEN_ENABLED", DataField.DataType.BOOLEAN, true); /** * Construct a relation prototype. Only called via {@link #assertRelation(Datastore, User)} @@ -116,6 +119,7 @@ protected RegisteredUsersTable(String schemaName) { fieldList.add(BASIC_AUTH_SALT); fieldList.add(DIGEST_AUTH_PASSWORD); fieldList.add(IS_REMOVED); + fieldList.add(GOOGLE_TOKEN_ENABLED); } /** @@ -223,7 +227,17 @@ public Boolean getIsRemoved() { public void setIsRemoved(Boolean value) { setBooleanField(IS_REMOVED, value); } - + + public boolean getGoogleTokenEnabled() { + Boolean value = getBooleanField(GOOGLE_TOKEN_ENABLED); + if ( value == null ) return false; + return value; + } + + public void setGoogleTokenEnabled(Boolean value) { + setBooleanField(GOOGLE_TOKEN_ENABLED, value); + } + private static RegisteredUsersTable relation = null; /** @@ -444,6 +458,8 @@ public static RegisteredUsersTable assertActiveUserByUserSecurityInfo(UserSecuri return r; } else { t.setFullName(u.getFullName()); + t.setGoogleTokenEnabled(( u.getUsername() == null ) && + u.getEnableGoogleAuthTokens()); ds.putEntity(t, user); return t; } diff --git a/src/main/java/org/opendatakit/common/security/spring/TargetUrlRequestAwareAuthenticationSuccessHandler.java b/src/main/java/org/opendatakit/common/security/spring/TargetUrlRequestAwareAuthenticationSuccessHandler.java new file mode 100644 index 0000000000..beeb5778b4 --- /dev/null +++ b/src/main/java/org/opendatakit/common/security/spring/TargetUrlRequestAwareAuthenticationSuccessHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2012 University of Washington. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.security.spring; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; +import org.springframework.util.StringUtils; + +/** + * Copied from SavedRequestAwareAuthenticationSuccessHandler + * Prefers using the redirect target URL (coming in as a query string parameter) + * over the saved request URL. + * + * @author mitchellsundt@gmail.com + * + */ +public class TargetUrlRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + protected final Log logger = LogFactory.getLog(this.getClass()); + + private RequestCache requestCache = new HttpSessionRequestCache(); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws ServletException, IOException { + SavedRequest savedRequest = requestCache.getRequest(request, response); + + String targetUrlParameter = getTargetUrlParameter(); + if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) { + requestCache.removeRequest(request, response); + super.onAuthenticationSuccess(request, response, authentication); + + return; + } + + // fall back to SimpleUrl actions only if no targetUrlParameter + if (savedRequest == null) { + super.onAuthenticationSuccess(request, response, authentication); + + return; + } + + clearAuthenticationAttributes(request); + + // Use the DefaultSavedRequest URL + String targetUrl = savedRequest.getRedirectUrl(); + logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } + + public void setRequestCache(RequestCache requestCache) { + this.requestCache = requestCache; + } +} \ No newline at end of file diff --git a/src/main/java/org/opendatakit/common/security/spring/UserDetailsServiceImpl.java b/src/main/java/org/opendatakit/common/security/spring/UserDetailsServiceImpl.java index d97d081451..710dd21fbc 100644 --- a/src/main/java/org/opendatakit/common/security/spring/UserDetailsServiceImpl.java +++ b/src/main/java/org/opendatakit/common/security/spring/UserDetailsServiceImpl.java @@ -52,7 +52,8 @@ enum PasswordType { enum CredentialType { Username, - Email + Email, + Token // Out-of-band (Oauth) or Oauth2 token }; private Datastore datastore; @@ -97,6 +98,8 @@ public void setCredentialType(String type) { credentialType = CredentialType.Username; } else if ( CredentialType.Email.name().equals(type)) { credentialType = CredentialType.Email; + } else if ( CredentialType.Token.name().equals(type)) { + credentialType = CredentialType.Token; } else { throw new IllegalArgumentException("Unrecognized CredentialType"); } @@ -143,7 +146,7 @@ public UserDetails loadUserByUsername(String name) final String password; final String salt; final Set grantedAuthorities; - final boolean isEnabled = true; + boolean isEnabled = true; final boolean isCredentialNonExpired = true; try { if ( credentialType == CredentialType.Username ) { @@ -182,7 +185,7 @@ public UserDetails loadUserByUsername(String name) "User " + name + " does not have a password configured. You must close and re-open your browser to clear this error."); } } else { - // openid... + // openid or token (oauth or oauth2)... // there is no password for an openid credential if ( passwordType != PasswordType.Random ) { throw new AuthenticationCredentialsNotFoundException( @@ -197,6 +200,10 @@ public UserDetails loadUserByUsername(String name) if ( eUser != null ) { uriUser = eUser.getUri(); grantedAuthorities = getGrantedAuthorities(eUser.getUri()); + // allow for Oauth and Oauth2 to be disabled. + if ( credentialType == CredentialType.Token ) { + isEnabled = eUser.getGoogleTokenEnabled(); + } } else { throw new UsernameNotFoundException("User " + name + " is not registered"); } diff --git a/src/main/java/org/opendatakit/common/utils/OutOfBandUserFetcher.java b/src/main/java/org/opendatakit/common/utils/OutOfBandUserFetcher.java new file mode 100644 index 0000000000..5abe4d318d --- /dev/null +++ b/src/main/java/org/opendatakit/common/utils/OutOfBandUserFetcher.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 University of Washington. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.utils; + +/** + * Interface for out-of-band retrieval of the current user's + * credentials. This is applicable to Gae deployments but other + * deployments through portals could potentially leverage this + * functionality. + * + * @author mitchellsundt@gmail.com + * + */ +public interface OutOfBandUserFetcher { + + /** + * Through means outside the webserver scope, obtain the + * logged-in user's email. This provides a bridge to the + * Google AppEngine OAuth service mechanism for OAuth + * manipulation of the AppEngine instance. + * + * @return the user's email string (mailto:username@domain.org) + */ + public String getEmail(); + +} diff --git a/src/main/java/org/opendatakit/common/utils/gae/GaeOutOfBandUserFetcher.java b/src/main/java/org/opendatakit/common/utils/gae/GaeOutOfBandUserFetcher.java new file mode 100644 index 0000000000..f0e6e55d97 --- /dev/null +++ b/src/main/java/org/opendatakit/common/utils/gae/GaeOutOfBandUserFetcher.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2012 University of Washington. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.utils.gae; + +import org.opendatakit.common.utils.OutOfBandUserFetcher; + +import com.google.appengine.api.oauth.OAuthRequestException; +import com.google.appengine.api.oauth.OAuthService; +import com.google.appengine.api.oauth.OAuthServiceFactory; +import com.google.appengine.api.users.User; + +/** + * Attempt to obtain a user e-mail string from the Google AppEngine + * OAuthService (Oauth 1.0). + * + * @author mitchellsundt@gmail.com + * + */ +public class GaeOutOfBandUserFetcher implements OutOfBandUserFetcher { + + @Override + public String getEmail() { + + try { + OAuthService authService = OAuthServiceFactory.getOAuthService(); + + User user = authService.getCurrentUser(); + + if ( user != null ) { + String email = user.getEmail(); + if ( email != null && email.length() != 0 ) { + return "mailto:" + email; + } + } + } catch ( OAuthRequestException e) { + // ignore this -- just means it isn't an OAuth-mediated request. + } + return null; + } + +} diff --git a/src/main/java/org/opendatakit/common/utils/tomcat/TomcatOutOfBandUserFetcher.java b/src/main/java/org/opendatakit/common/utils/tomcat/TomcatOutOfBandUserFetcher.java new file mode 100644 index 0000000000..ff343f0fd6 --- /dev/null +++ b/src/main/java/org/opendatakit/common/utils/tomcat/TomcatOutOfBandUserFetcher.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 University of Washington. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.opendatakit.common.utils.tomcat; + +import org.opendatakit.common.utils.OutOfBandUserFetcher; + +/** + * No-op implementation of the OutOfBandUserFetcher for Tomcat deployments. + * + * @author mitchellsundt@gmail.com + * + */ +public class TomcatOutOfBandUserFetcher implements OutOfBandUserFetcher { + + @Override + public String getEmail() { + return null; + } + +} diff --git a/src/main/java/org/opendatakit/common/web/servlet/CommonServletBase.java b/src/main/java/org/opendatakit/common/web/servlet/CommonServletBase.java index a7a07f3039..6295e1aa40 100644 --- a/src/main/java/org/opendatakit/common/web/servlet/CommonServletBase.java +++ b/src/main/java/org/opendatakit/common/web/servlet/CommonServletBase.java @@ -25,6 +25,8 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; import java.util.TimeZone; import javax.servlet.http.HttpServlet; @@ -32,6 +34,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.apache.commons.logging.Log; import org.opendatakit.aggregate.constants.ServletConsts; import org.opendatakit.common.security.spring.SpringInternals; import org.opendatakit.common.web.CallingContext; @@ -60,6 +63,43 @@ protected CommonServletBase(String applicationName) { this.applicationName = applicationName; } + protected Map parseParameterMap(HttpServletRequest request, Log logger) { + + Map parameters = new HashMap(); + @SuppressWarnings("rawtypes") + Map m = request.getParameterMap(); + for ( Object eo : m.entrySet() ) { + @SuppressWarnings("unchecked") + Map.Entry e = (Map.Entry) eo; + Object k = e.getKey(); + Object v = e.getValue(); + String key = null; + String value = null; + if ( k instanceof String ) { + key = (String) k; + } else { + logger.error("key is not a string: " + k.getClass().getCanonicalName()); + } + if ( v instanceof String[] ) { + String[] va = (String[]) v; + if ( va.length == 1 ) { + value = va[0]; + } else if ( va.length != 0 ) { + logger.error("v is an array of string of length: " + va.length); + value = va[0]; + } + } else if ( v instanceof String ) { + value = (String) v; + } else { + logger.error("v is not a string: " + v.getClass().getCanonicalName()); + } + if ( key != null && value != null ) { + parameters.put(key, value); + } + } + return parameters; + } + protected String getRedirectUrl(HttpServletRequest request) { HttpSession session = request.getSession(false); if(session != null) { diff --git a/src/main/resources/gae-unit/odk-settings.xml b/src/main/resources/gae-unit/odk-settings.xml index 850d6d22c8..a26a705d3b 100644 --- a/src/main/resources/gae-unit/odk-settings.xml +++ b/src/main/resources/gae-unit/odk-settings.xml @@ -51,6 +51,7 @@ + diff --git a/src/main/resources/gae/odk-settings.xml b/src/main/resources/gae/odk-settings.xml index 9b5925adc9..40c47afd86 100644 --- a/src/main/resources/gae/odk-settings.xml +++ b/src/main/resources/gae/odk-settings.xml @@ -49,6 +49,7 @@ + diff --git a/src/main/resources/mysql-unit/odk-settings.xml b/src/main/resources/mysql-unit/odk-settings.xml index 3be9bcab7d..01f8fab767 100644 --- a/src/main/resources/mysql-unit/odk-settings.xml +++ b/src/main/resources/mysql-unit/odk-settings.xml @@ -69,6 +69,7 @@ + diff --git a/src/main/resources/mysql/odk-settings.xml b/src/main/resources/mysql/odk-settings.xml index ab5ea82703..fe3ecdd396 100644 --- a/src/main/resources/mysql/odk-settings.xml +++ b/src/main/resources/mysql/odk-settings.xml @@ -70,6 +70,7 @@ + diff --git a/src/main/resources/postgres-unit/odk-settings.xml b/src/main/resources/postgres-unit/odk-settings.xml index f4c0fae9aa..925144aa4c 100644 --- a/src/main/resources/postgres-unit/odk-settings.xml +++ b/src/main/resources/postgres-unit/odk-settings.xml @@ -69,6 +69,7 @@ + diff --git a/src/main/resources/postgres/odk-settings.xml b/src/main/resources/postgres/odk-settings.xml index d179d5a6fe..22b66fbe5c 100644 --- a/src/main/resources/postgres/odk-settings.xml +++ b/src/main/resources/postgres/odk-settings.xml @@ -70,6 +70,7 @@ +