Skip to content

Commit

Permalink
enh: introduced WrappedSecurityManager to core and refactored Securit…
Browse files Browse the repository at this point in the history
…yUtils.getSecurityManager() and Jakarta EE to use it
  • Loading branch information
lprimak committed Apr 21, 2024
1 parent 10bb454 commit 878447b
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 39 deletions.
25 changes: 25 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- collect the test classes so they can be referenced by other modules -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down Expand Up @@ -169,6 +181,19 @@
<artifactId>log4j-core-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>

<!-- JDBC Realm tests: -->
<dependency>
Expand Down
65 changes: 65 additions & 0 deletions core/src/main/java/org/apache/shiro/SecurityUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
package org.apache.shiro;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.WrappedSecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import java.util.Objects;
import java.util.function.Predicate;


/**
Expand Down Expand Up @@ -123,4 +126,66 @@ public static SecurityManager getSecurityManager() throws UnavailableSecurityMan
}
return securityManager;
}

/**
* Returns the SecurityManager, ensuring it is of the specified type.
* Unwraps wrapped SecurityManagers if necessary.
* Caution, since this method unwraps SecurityManagers, it is possible that
* functionality of the wrapper is lost by the returned instance.
*
* @param type the expected type of the SecurityManager
* @return the SecurityManager.
* @param <SM> the expected type of the SecurityManager
*/
public static <SM extends SecurityManager> SM getSecurityManager(Class<SM> type) {
Objects.requireNonNull(type, "Class argument cannot be null.");
return unwrapSecurityManager(getSecurityManager(), type);
}

/**
* Determines if the specified security manager is of the specified type or a subclass of the specified type.
*
* @param securityManager
* @param type
* @return true if the security manager is of the specified type or a subclass of the specified type, false otherwise.
*/
public static boolean isSecurityManagerTypeOf(SecurityManager securityManager,
Class<? extends SecurityManager> type) {
return type.isAssignableFrom(unwrapSecurityManager(securityManager, type).getClass());
}

/**
* Unwraps wrapped SecurityManagers if necessary.
* @param securityManager the SecurityManager to unwrap
* @param type the expected type of the SecurityManager
* @return the unwrapped SecurityManager
* @param <SM> Type of the SecurityManager
*/
public static <SM extends SecurityManager> SM
unwrapSecurityManager(SecurityManager securityManager, Class<SM> type) {
return unwrapSecurityManager(securityManager, type, type::isAssignableFrom);
}

/**
* Unwraps wrapped SecurityManagers if necessary.
* @param securityManager the SecurityManager to unwrap
* @param type the expected type of the SecurityManager
* @param predicate to determine if the SecurityManager is of the expected type
* @return the unwrapped SecurityManager
* @param <SM> Type of the SecurityManager
*/
@SuppressWarnings("unchecked")
public static <SM extends SecurityManager> SM
unwrapSecurityManager(SecurityManager securityManager, Class<SM> type,
Predicate<Class<? extends SecurityManager>> predicate) {
while (securityManager instanceof WrappedSecurityManager && !predicate.test(securityManager.getClass())) {
WrappedSecurityManager wrappedSecurityManager = (WrappedSecurityManager) securityManager;
securityManager = wrappedSecurityManager.unwrap();
if (securityManager == wrappedSecurityManager) {
throw new IllegalStateException("SecurityManager implementation of type [" + type.getName()
+ "] is wrapped by itself, which is an invalid configuration.");
}
}
return (SM) securityManager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.shiro.mgt;

/**
* Interface implemented by {@link SecurityManager} implementations that wrap another {@code SecurityManager} instance.
*/
public interface WrappedSecurityManager {
/**
* Returns the underlying {@code SecurityManager} instance that this instance wraps.
*
* @return instance
* @param <SM> {@link SecurityManager} implementation type
*/
<SM extends SecurityManager> SM unwrap();
}
151 changes: 151 additions & 0 deletions core/src/test/java/org/apache/shiro/SecurityUtilsUnwrapTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.shiro;

import lombok.RequiredArgsConstructor;
import lombok.experimental.Delegate;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.mgt.WrappedSecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.apache.shiro.SecurityUtils.getSecurityManager;
import static org.apache.shiro.SecurityUtils.isSecurityManagerTypeOf;
import static org.apache.shiro.SecurityUtils.unwrapSecurityManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SecurityUtilsUnwrapTest {
@Mock
SecurityManager securityManager;
@Mock
DefaultSecurityManager defaultSecurityManager;
@Mock
Subject subject;
@Mock
SubjectContext subjectContext;

@RequiredArgsConstructor
static class Wrapped implements WrappedSecurityManager, SecurityManager {
private final @Delegate SecurityManager securityManager;

@Override
@SuppressWarnings("unchecked")
public <SM extends SecurityManager> SM unwrap() {
return (SM) securityManager;
}
}

@RequiredArgsConstructor
static class InvalidWrapped implements WrappedSecurityManager, SecurityManager {
private final @Delegate SecurityManager securityManager;

@Override
@SuppressWarnings("unchecked")
public <SM extends SecurityManager> SM unwrap() {
return (SM) this;
}
}

@Test
void basicUnwrap() {
SecurityManager sm = unwrapSecurityManager(securityManager, SecurityManager.class);
assertEquals(securityManager, sm);
}

@Test
void basicTypeCheck() {
assertTrue(isSecurityManagerTypeOf(securityManager, SecurityManager.class));
}

@Test
void securityManager() {
try (var threadContext = mockStatic(ThreadContext.class)) {
threadContext.when(ThreadContext::getSecurityManager).thenReturn(defaultSecurityManager);
DefaultSecurityManager dsm = getSecurityManager(DefaultSecurityManager.class);
assertEquals(defaultSecurityManager, dsm);
}
}

@Test
void failedTypeUnwrap() {
assertThrows(ClassCastException.class, () -> {
SessionsSecurityManager ssm = unwrapSecurityManager(securityManager, SessionsSecurityManager.class);
});
}

@Test
void defaultSecurityManager() {
var dsm = unwrapSecurityManager(defaultSecurityManager, DefaultSecurityManager.class);
assertEquals(defaultSecurityManager, dsm);
when(defaultSecurityManager.createSubject(subjectContext)).thenReturn(subject);
Subject subject = dsm.createSubject(subjectContext);
assertEquals(this.subject, subject);
verify(defaultSecurityManager).createSubject(subjectContext);
verifyNoMoreInteractions(defaultSecurityManager, this.subject, subjectContext);
}

@Test
void invalidCast() {
SecurityManager wrapped = new Wrapped(defaultSecurityManager);
assertThrows(ClassCastException.class, () -> {
DefaultSecurityManager sm = (DefaultSecurityManager) wrapped;
});
}

@Test
void unwrapOne() {
SecurityManager wrapped = new Wrapped(defaultSecurityManager);
assertEquals(defaultSecurityManager, unwrapSecurityManager(wrapped, DefaultSecurityManager.class));
}

@Test
void unwrapTwo() {
SecurityManager wrapped = new Wrapped(new Wrapped(defaultSecurityManager));
assertEquals(defaultSecurityManager, unwrapSecurityManager(wrapped, DefaultSecurityManager.class));
}

@Test
void invalidWrap() {
SecurityManager wrapped = new Wrapped(new InvalidWrapped(defaultSecurityManager));
assertThrows(IllegalStateException.class, () -> {
assertEquals(defaultSecurityManager, unwrapSecurityManager(wrapped, DefaultSecurityManager.class));
});
}

@Test
void invalidWrapInverted() {
SecurityManager wrapped = new InvalidWrapped(new Wrapped(defaultSecurityManager));
assertThrows(IllegalStateException.class, () -> {
assertEquals(defaultSecurityManager, unwrapSecurityManager(wrapped, DefaultSecurityManager.class));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
*/
package org.apache.shiro.ee.filters;

import static org.apache.shiro.SecurityUtils.getSecurityManager;
import static org.apache.shiro.SecurityUtils.isSecurityManagerTypeOf;
import static org.apache.shiro.SecurityUtils.unwrapSecurityManager;
import static org.apache.shiro.ee.filters.FormAuthenticationFilter.LOGIN_URL_ATTR_NAME;
import static org.apache.shiro.ee.filters.FormResubmitSupport.HttpHeaderContstants.CONTENT_TYPE;
import static org.apache.shiro.ee.filters.FormResubmitSupport.HttpHeaderContstants.COOKIE;
Expand All @@ -33,8 +36,6 @@
import java.util.Collections;
import org.apache.shiro.ee.filters.Forms.FallbackPredicate;
import static org.apache.shiro.ee.filters.FormResubmitSupportCookies.transformCookieHeader;
import static org.apache.shiro.ee.filters.ShiroFilter.isSecurityManagerTypeOf;
import static org.apache.shiro.ee.filters.ShiroFilter.unwrapSecurityManager;
import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.isFormResubmitDisabled;
import java.io.IOException;
import java.net.CookieManager;
Expand Down Expand Up @@ -138,11 +139,11 @@ static class PartialAjaxResult {
}

static void savePostDataForResubmit(HttpServletRequest request, HttpServletResponse response, @NonNull String loginUrl) {
if (isPostRequest(request) && isSecurityManagerTypeOf(SecurityUtils.getSecurityManager(),
if (isPostRequest(request) && isSecurityManagerTypeOf(getSecurityManager(),
DefaultSecurityManager.class)) {
String postData = getPostData(request);
var cacheKey = UUID.randomUUID();
DefaultSecurityManager dsm = unwrapSecurityManager(SecurityUtils.getSecurityManager());
DefaultSecurityManager dsm = getSecurityManager(DefaultSecurityManager.class);
if (dsm.getCacheManager() != null) {
var cache = dsm.getCacheManager().getCache(FORM_DATA_CACHE);
var rememberMeManager = (AbstractRememberMeManager) dsm.getRememberMeManager();
Expand Down Expand Up @@ -180,8 +181,8 @@ static String getPostData(ServletRequest request) {

static String getSavedFormDataFromKey(@NonNull String savedFormDataKey) {
String savedFormData = null;
if (isSecurityManagerTypeOf(SecurityUtils.getSecurityManager(), DefaultSecurityManager.class)) {
DefaultSecurityManager dsm = unwrapSecurityManager(SecurityUtils.getSecurityManager());
if (isSecurityManagerTypeOf(getSecurityManager(), DefaultSecurityManager.class)) {
DefaultSecurityManager dsm = getSecurityManager(DefaultSecurityManager.class);
if (dsm.getCacheManager() != null) {
var cache = dsm.getCacheManager().getCache(FORM_DATA_CACHE);
var cacheKey = UUID.fromString(savedFormDataKey);
Expand Down Expand Up @@ -212,7 +213,7 @@ static void saveRequest(HttpServletRequest request, HttpServletResponse response
Servlets.addResponseCookie(request, response, WebUtils.SAVED_REQUEST_KEY,
path, null, request.getContextPath(),
// cookie age = session timeout
getCookieAge(request, SecurityUtils.getSecurityManager()));
getCookieAge(request, getSecurityManager()));
}
}

Expand Down Expand Up @@ -478,7 +479,7 @@ private static String processResubmitResponse(HttpResponse<String> response,
// do not duplicate the session cookie(s)
transformCookieHeader(headers.allValues(SET_COOKIE))
.entrySet().stream().filter(not(entry -> entry.getKey()
.startsWith(getSessionCookieName(servletContext, SecurityUtils.getSecurityManager()))))
.startsWith(getSessionCookieName(servletContext, getSecurityManager()))))
.forEach(entry -> addCookie(originalResponse, servletContext,
entry.getKey(), entry.getValue(), -1));
if (isPartialAjaxRequest) {
Expand Down Expand Up @@ -508,7 +509,7 @@ private static HttpClient buildHttpClient(URI savedRequest, ServletContext servl
HttpServletRequest originalRequest) {
CookieManager cookieManager = new CookieManager();
var session = SecurityUtils.getSubject().getSession();
var sessionCookieName = getSessionCookieName(servletContext, SecurityUtils.getSecurityManager());
var sessionCookieName = getSessionCookieName(servletContext, getSecurityManager());
var sessionCookie = new HttpCookie(sessionCookieName, session.getId().toString());
sessionCookie.setPath(servletContext.getContextPath());
sessionCookie.setVersion(0);
Expand All @@ -533,7 +534,7 @@ private static HttpClient buildHttpClient(URI savedRequest, ServletContext servl

public static DefaultWebSessionManager getNativeSessionManager(SecurityManager securityManager) {
DefaultWebSessionManager rv = null;
SecurityManager unwrapped = unwrapSecurityManager(securityManager);
SecurityManager unwrapped = unwrapSecurityManager(securityManager, SecurityManager.class, type -> false);
if (unwrapped instanceof SessionsSecurityManager) {
var ssm = (SessionsSecurityManager) unwrapped;
var sm = ssm.getSessionManager();
Expand Down
Loading

0 comments on commit 878447b

Please sign in to comment.