Skip to content

Commit

Permalink
Added impersonation support on spring-security filters
Browse files Browse the repository at this point in the history
  • Loading branch information
sergey-podolsky committed Jan 17, 2016
1 parent ec673b2 commit fbc311a
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 45 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ Waffle.sln.cache
Waffle.suo
*.csproj.user
.tern-project
*.iml
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

* Rework .net build to be mostly automatic using nuget
* Change .net target to more modern .net 4.0 framework
* [#309](https://github.com/dblock/waffle/pull/309): Added impersonation support on spring-security filters [@sergey-podolsky](https://github.com/sergey-podolsky).
* [#296](https://github.com/dblock/waffle/pull/296): Added Tomcat 9 support.
* [#268](https://github.com/dblock/waffle/pull/301): Cannot log in automatically on machine where Tomcat service is running
* [#274](https://github.com/dblock/waffle/pull/274): Update WindowsSecurityContextImpl.java to handle SEC_E_BUFFER_TOO_SMALL
Expand Down
2 changes: 2 additions & 0 deletions Docs/spring/SpringSecuritySingleSignOnFilter.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,15 @@ The `waffleNegotiateSecurityFilter` bean can be configured with the following op
* principalFormat: Specifies the name format for the principal.
* roleFormat: Specifies the name format for the role.
* allowGuestLogin: Allow guest login. When true and the system's Guest account is enabled, any invalid login succeeds as Guest. Note that while the default value of allowGuestLogin is true, it is recommended that you disable the system's Guest account to disallow Guest login. This option is provided for systems where you don't have administrative privileges.
* impersonate: Allow impersonation. When true the remote user will be impersonated. Note that there is no mapping between the Windows native threads, under which the impersonation takes place, and the Java threads. Thus you'll need to use Windows native APIs to perform impersonated actions. Any action done in Java will still be performed with the user account running the servlet container.
* defaultGrantedAuthority: Specifies the `GrantedAuthority` to be added to every successfully authenticated user. By default, the `defaultGrantedAuthority` will add a `GrantedAuthority` for `ROLE_USER`. If you do not want this behavior, you can set the `defaultGrantedAuthority` to `null` (if you do not want a `GrantedAuthority` to be added by default), or some other `GrantedAuthority`.
* grantedAuthorityFactory: Used to create `GrantedAuthority` objects for each of the groups to which the authenticated user belongs. The default `grantedAuthorityFactory` will construct `GrantedAuthority` objects whose string is the uppercase group name prefixed with `ROLE_`.

``` xml
<bean id="waffleNegotiateSecurityFilter" class="waffle.spring.NegotiateSecurityFilter">
<property name="Provider" ref="waffleSecurityFilterProviderCollection" />
<property name="AllowGuestLogin" value="false" />
<property name="Impersonate" value="true" />
<property name="PrincipalFormat" value="fqn" />
<property name="RoleFormat" value="both" />
</bean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import waffle.servlet.AutoDisposableWindowsPrincipal;
import waffle.servlet.WindowsPrincipal;
import waffle.servlet.spi.SecurityFilterProviderCollection;
import waffle.util.AuthorizationHeader;
import waffle.windows.auth.IWindowsIdentity;
import waffle.windows.auth.IWindowsImpersonationContext;
import waffle.windows.auth.PrincipalFormat;

/**
Expand All @@ -59,6 +61,9 @@ public class NegotiateSecurityFilter extends GenericFilterBean {
/** The allow guest login. */
private boolean allowGuestLogin = true;

/** The impersonate. */
private boolean impersonate;

/** The granted authority factory. */
private GrantedAuthorityFactory grantedAuthorityFactory = WindowsAuthenticationToken.DEFAULT_GRANTED_AUTHORITY_FACTORY;

Expand Down Expand Up @@ -115,12 +120,14 @@ public void doFilter(final ServletRequest req, final ServletResponse res, final
return;
}

IWindowsImpersonationContext ctx = null;
try {
NegotiateSecurityFilter.LOGGER.debug("logged in user: {} ({})", windowsIdentity.getFqn(),
windowsIdentity.getSidString());

final WindowsPrincipal principal = new WindowsPrincipal(windowsIdentity, this.principalFormat,
this.roleFormat);
final WindowsPrincipal principal = impersonate ? new AutoDisposableWindowsPrincipal(windowsIdentity,
this.principalFormat, this.roleFormat) : new WindowsPrincipal(windowsIdentity,
this.principalFormat, this.roleFormat);

NegotiateSecurityFilter.LOGGER.debug("roles: {}", principal.getRolesString());

Expand All @@ -133,12 +140,23 @@ public void doFilter(final ServletRequest req, final ServletResponse res, final

NegotiateSecurityFilter.LOGGER.info("successfully logged in user: {}", windowsIdentity.getFqn());

if (this.impersonate) {
LOGGER.debug("impersonating user");
ctx = windowsIdentity.impersonate();
}

chain.doFilter(request, response);
} finally {
windowsIdentity.dispose();
if (this.impersonate && ctx != null) {
NegotiateSecurityFilter.LOGGER.debug("terminating impersonation");
ctx.revertToSelf();
} else {
windowsIdentity.dispose();
}
}
} else {
chain.doFilter(request, response);
}

chain.doFilter(request, response);
}

/*
Expand Down Expand Up @@ -276,6 +294,25 @@ public void setAllowGuestLogin(final boolean value) {
this.allowGuestLogin = value;
}

/**
* Enable/Disable impersonation.
*
* @param value
* true to enable impersonation, false otherwise
*/
public void setImpersonate(final boolean value) {
this.impersonate = value;
}

/**
* Checks if is impersonate.
*
* @return true if impersonation is enabled, false otherwise
*/
public boolean isImpersonate() {
return this.impersonate;
}

/**
* Gets the provider.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* Waffle (https://github.com/dblock/waffle)
*
* Copyright (c) 2010 - 2015 Application Security, Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Application Security, Inc.
*/
package waffle.spring;

import com.google.common.base.Charsets;
import com.google.common.io.BaseEncoding;
import com.sun.jna.WString;
import com.sun.jna.platform.win32.Advapi32Util;
import com.sun.jna.platform.win32.LMAccess;
import com.sun.jna.platform.win32.LMErr;
import com.sun.jna.platform.win32.Netapi32;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import waffle.mock.MockWindowsAccount;
import waffle.mock.http.RecordUserNameFilterChain;
import waffle.mock.http.SimpleHttpRequest;
import waffle.mock.http.SimpleHttpResponse;
import waffle.servlet.AutoDisposableWindowsPrincipal;
import waffle.servlet.WindowsPrincipal;
import waffle.servlet.spi.SecurityFilterProviderCollection;
import waffle.windows.auth.impl.WindowsAuthProviderImpl;

import javax.servlet.ServletException;
import java.io.IOException;
import java.security.Principal;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;

/**
* The Class ImpersonateTests.
*/
public class ImpersonateTests {

/** The filter. */
private waffle.spring.NegotiateSecurityFilter filter;

/** The user info. */
private LMAccess.USER_INFO_1 userInfo;

/** The result of net add user. */
private int resultOfNetAddUser;

/**
* Sets the up.
*/
@Before
public void setUp() {
this.filter = new NegotiateSecurityFilter();
this.filter.setProvider(new SecurityFilterProviderCollection(new WindowsAuthProviderImpl()));

this.userInfo = new LMAccess.USER_INFO_1();
this.userInfo.usri1_name = new WString(MockWindowsAccount.TEST_USER_NAME);
this.userInfo.usri1_password = new WString(MockWindowsAccount.TEST_PASSWORD);
this.userInfo.usri1_priv = LMAccess.USER_PRIV_USER;

this.resultOfNetAddUser = Netapi32.INSTANCE.NetUserAdd(null, 1, this.userInfo, null);
Assume.assumeThat("Unable to add user (need to be administrator to do this).", this.resultOfNetAddUser,
is(LMErr.NERR_Success));
}

/**
* Tear down.
*/
@After
public void tearDown() {
this.filter.destroy();

if (LMErr.NERR_Success == this.resultOfNetAddUser) {
Assert.assertEquals(LMErr.NERR_Success,
Netapi32.INSTANCE.NetUserDel(null, this.userInfo.usri1_name.toString()));
}
}

/**
* Test impersonate enabled.
*
* @throws IOException
* Signals that an I/O exception has occurred.
* @throws ServletException
* the servlet exception
*/
@Test
public void testImpersonateEnabled() throws IOException, ServletException {

assertThat(Advapi32Util.getUserName())
.isNotEqualTo(MockWindowsAccount.TEST_USER_NAME)
.withFailMessage("Current user shouldn't be the test user prior to the test");

final SimpleHttpRequest request = new SimpleHttpRequest();
request.setMethod("GET");
final String userHeaderValue = MockWindowsAccount.TEST_USER_NAME + ":" + MockWindowsAccount.TEST_PASSWORD;
final String basicAuthHeader = "Basic "
+ BaseEncoding.base64().encode(userHeaderValue.getBytes(Charsets.UTF_8));
request.addHeader("Authorization", basicAuthHeader);

final SimpleHttpResponse response = new SimpleHttpResponse();
final RecordUserNameFilterChain filterChain = new RecordUserNameFilterChain();

this.filter.setImpersonate(true);
this.filter.doFilter(request, response, filterChain);

final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Assert.assertTrue("Test user should be authenticated", authentication.isAuthenticated());

final Principal principal = (Principal) authentication.getPrincipal();
assertThat(principal).isInstanceOf(AutoDisposableWindowsPrincipal.class);
AutoDisposableWindowsPrincipal windowsPrincipal = (AutoDisposableWindowsPrincipal) principal;
try {
assertThat(filterChain.getUserName())
.isEqualTo(MockWindowsAccount.TEST_USER_NAME)
.withFailMessage("Test user should be impersonated");
assertThat(Advapi32Util.getUserName())
.isNotEqualTo(MockWindowsAccount.TEST_USER_NAME)
.withFailMessage("Impersonation context should have been reverted");
} finally {
windowsPrincipal.getIdentity().dispose();
}
}

/**
* Test impersonate disabled.
*
* @throws IOException
* Signals that an I/O exception has occurred.
* @throws ServletException
* the servlet exception
*/
@Test
public void testImpersonateDisabled() throws IOException, ServletException {

assertThat(Advapi32Util.getUserName())
.isNotEqualTo(MockWindowsAccount.TEST_USER_NAME)
.withFailMessage("Current user shouldn't be the test user prior to the test");
final SimpleHttpRequest request = new SimpleHttpRequest();
request.setMethod("GET");
final String userHeaderValue = MockWindowsAccount.TEST_USER_NAME + ":" + MockWindowsAccount.TEST_PASSWORD;
final String basicAuthHeader = "Basic "
+ BaseEncoding.base64().encode(userHeaderValue.getBytes(Charsets.UTF_8));
request.addHeader("Authorization", basicAuthHeader);
final SimpleHttpResponse response = new SimpleHttpResponse();
final RecordUserNameFilterChain filterChain = new RecordUserNameFilterChain();

this.filter.setImpersonate(false);
this.filter.doFilter(request, response, filterChain);

final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Assert.assertTrue("Test user should be authenticated", authentication.isAuthenticated());

final Principal principal = (Principal) authentication.getPrincipal();
assertThat(principal).isInstanceOf(WindowsPrincipal.class);
WindowsPrincipal windowsPrincipal = (WindowsPrincipal) principal;
try {
assertThat(filterChain.getUserName())
.isNotEqualTo(MockWindowsAccount.TEST_USER_NAME)
.withFailMessage("Test user should not be impersonated");
assertThat(Advapi32Util.getUserName())
.isNotEqualTo(MockWindowsAccount.TEST_USER_NAME)
.withFailMessage("Impersonation context should have been reverted");
} finally {
windowsPrincipal.getIdentity().dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import waffle.servlet.AutoDisposableWindowsPrincipal;
import waffle.servlet.WindowsPrincipal;
import waffle.servlet.spi.SecurityFilterProviderCollection;
import waffle.util.AuthorizationHeader;
import waffle.windows.auth.IWindowsIdentity;
import waffle.windows.auth.IWindowsImpersonationContext;
import waffle.windows.auth.PrincipalFormat;

/**
Expand All @@ -59,6 +61,9 @@ public class NegotiateSecurityFilter extends GenericFilterBean {
/** The allow guest login. */
private boolean allowGuestLogin = true;

/** The impersonate. */
private boolean impersonate;

/** The granted authority factory. */
private GrantedAuthorityFactory grantedAuthorityFactory = WindowsAuthenticationToken.DEFAULT_GRANTED_AUTHORITY_FACTORY;

Expand Down Expand Up @@ -115,12 +120,14 @@ public void doFilter(final ServletRequest req, final ServletResponse res, final
return;
}

IWindowsImpersonationContext ctx = null;
try {
NegotiateSecurityFilter.LOGGER.debug("logged in user: {} ({})", windowsIdentity.getFqn(),
windowsIdentity.getSidString());

final WindowsPrincipal principal = new WindowsPrincipal(windowsIdentity, this.principalFormat,
this.roleFormat);
final WindowsPrincipal principal = impersonate ? new AutoDisposableWindowsPrincipal(windowsIdentity,
this.principalFormat, this.roleFormat) : new WindowsPrincipal(windowsIdentity,
this.principalFormat, this.roleFormat);

NegotiateSecurityFilter.LOGGER.debug("roles: {}", principal.getRolesString());

Expand All @@ -133,12 +140,23 @@ public void doFilter(final ServletRequest req, final ServletResponse res, final

NegotiateSecurityFilter.LOGGER.info("successfully logged in user: {}", windowsIdentity.getFqn());

if (this.impersonate) {
LOGGER.debug("impersonating user");
ctx = windowsIdentity.impersonate();
}

chain.doFilter(request, response);
} finally {
windowsIdentity.dispose();
if (this.impersonate && ctx != null) {
NegotiateSecurityFilter.LOGGER.debug("terminating impersonation");
ctx.revertToSelf();
} else {
windowsIdentity.dispose();
}
}
} else {
chain.doFilter(request, response);
}

chain.doFilter(request, response);
}

/*
Expand Down Expand Up @@ -276,6 +294,25 @@ public void setAllowGuestLogin(final boolean value) {
this.allowGuestLogin = value;
}

/**
* Enable/Disable impersonation.
*
* @param value
* true to enable impersonation, false otherwise
*/
public void setImpersonate(final boolean value) {
this.impersonate = value;
}

/**
* Checks if is impersonate.
*
* @return true if impersonation is enabled, false otherwise
*/
public boolean isImpersonate() {
return this.impersonate;
}

/**
* Gets the provider.
*
Expand Down
Loading

0 comments on commit fbc311a

Please sign in to comment.