AppSensor is intrusion detection framework described in an another post. Out of the box version assumes that underlying application supports ESAPI interfaces. In this post, we will take an application secured by Shiro framework which does not support ESAPI and integrate it with AppSensor.
This post is only about integration. It does not show how to add AppSensor to the application, nor what it is, nor how to use it. All that can be found in AppSensor - Intrusion Detection post.
Example Application
We will add intrusion detection into SimpleShiroSecuredApplication. You can download it from intrusion_detection branch on Github. Use test class RunTestWait to run web server with application deployed on https://localhost:8443/simpleshirosecuredapplication/ URL.
The application has seven users: administrator, mathematician, physicien, friendlyrepairman, unfriendlyrepairman, productsales and servicessales. They all share the same password: 'heslo'.
Introduction
Intrusion detection framework access application data through ASUtilities interface. We will override its default implementation with one that delegates calls to Shiro wherever possible. The rest will be implemented in simplest possible way.
The interface provides access to:
- security events logger,
- current HTTP request,
- logged user.
Logger
Security events logger interface ASLogger is similar to any other java logger interface. The method error is supposed to log errors, the method info is supposed to log info and so on. Our IntrusionDetectionLogger delegates all calls to standard logger:
public class IntrusionDetectionLogger implements ASLogger { private static final Logger log = LoggerFactory.getLogger( IntrusionDetectionLogger.class ); ... @Override public void warning(String message, Throwable throwable) { log.warn(message, throwable); } ... }
Http Request
Getting ASUtilities to return HttpRequest is little bit more complicated. Shiro is general purpose framework and does not provide access to http request from its security utilities.
We have to create servlet request filter and configure it to run first for each incoming http request. The filter binds incoming http request object to current thread. When AppSensor asks for http request object, our ASUtilities implementation acquires it from the thread variables.
The class AppSensorIntegrationThreadContext stores and acquires requests. The class is basically a copy of Shiro ThreadContext class with some methods deleted, so beware of licensing issues if you plan to use it. We have to create new storage, because Shiro servlet filter always remove all content from ThreadContext.
New servlet filter calls it to bind request to the current thread:
public class ShiroAppSensorIntegrationFilter extends AdviceFilter { @Override protected boolean preHandle( ... ) throws Exception { AppSensorIntegrationThreadContext.remove(); AppSensorIntegrationThreadContext.setCurrentRequest((HttpServletRequest)request); return true; } }
Filter is configured in web.xml to run before any other filter:
ShiroAppSensorIntegrationFilter org.meri.simpleshirosecuredapplication.intrusiondetection. integration.ShiroAppSensorIntegrationFilter ... other filters ... ShiroAppSensorIntegrationFilter /*
Logged User
The longest object returned by ASUtilities is ASUser object. It represents currently logged user and roughly corresponds to Shiro Subject concept. Therefore, we will turn Subject into an ASUser.
Logged Subject
AppSensor needs to know whether current user is known or anonymous and who it is. If the user is known, the framework needs to know his name and unique id. Shiro subject has neither name, nor unique id. Instead, it has primary principal. Primary principal can be any object and uniquely identifies user account.
If the primary principal is null, the user is anonymous:
If the primary principal is null, the user is anonymous:
private final Subject subject; @Override public boolean isAnonymous() { return subject.getPrincipal()==null; }
We use primary principals hash code as account id and name as account name:
private final Subject subject; @Override public long getAccountId() { Object principal = subject.getPrincipal(); return principal==null ? 0 : principal.hashCode(); } @Override public String getAccountName() { Object primaryPrincipal = subject.getPrincipal(); if (primaryPrincipal == null) return null; Object property = null; try { property = PropertyUtils.getProperty(primaryPrincipal, "name"); } catch (Exception e) { } return property == null ? primaryPrincipal.toString() : property.toString(); }
Log Out
The method logout of ASUser delegates the call to the Subject:private final Subject subject; @Override public void logout() { subject.logout(); }
Disable Account
ASUser interface has two methods with no Shiro equivalent: disable and isDisabled. As the names suggest, first one is able to disable user account and second one returns whether an account is disabled. In this chapter, we will integrate previously created InMemoryDisabler into Shiro.
InMemoryDisabler keeps list of all disabled accounts in cache. It is unusable for any real-world application, but simple enough to serve as an example.
All Shiro security features are accessible through an instance of SecurityManager. Its default implementation delegates authentication to ModularRealmAuthenticator class. Disable account feature requires change in both classes. First, we will override the authenticator to verify disabled/enables status of an account after each login. Second, we will create DisablingSecurityManager interface and its implementation which will:
ModularRealmAuthenticator authenticates users in doAuthenticate method. Our version adds account verification to it:
InMemoryDisabler keeps list of all disabled accounts in cache. It is unusable for any real-world application, but simple enough to serve as an example.
All Shiro security features are accessible through an instance of SecurityManager. Its default implementation delegates authentication to ModularRealmAuthenticator class. Disable account feature requires change in both classes. First, we will override the authenticator to verify disabled/enables status of an account after each login. Second, we will create DisablingSecurityManager interface and its implementation which will:
- use DisablingModularRealAuthenticator to authenticate users,
- manage disabler instance,
- expose disable and isDisabled methods to the application.
ModularRealmAuthenticator authenticates users in doAuthenticate method. Our version adds account verification to it:
public class DisablingModularRealmAuthenticator extends ModularRealmAuthenticator { private final Disabler disabler; public DisablingModularRealmAuthenticator(Disabler disabler) { super(); this.disabler = disabler; } @Override protected AuthenticationInfo doAuthenticate(...) ... { AuthenticationInfo info = super.doAuthenticate(authenticationToken); if (disabler.isDisabled(info)) { throw new AuthenticationException("The account has been disabled."); } return info; } }
DisablingWebSecurityManager replaces original authenticator with DisablingModularRealmAuthenticator:
public class DisablingWebSecurityManager extends DefaultWebSecurityManager implements DisablingSecurityManager { public DisablingWebSecurityManager() { disabler = new InMemoryDisabler(); setAuthenticator(new DisablingModularRealmAuthenticator(disabler)); } }
supplies InMemory disabler with active cache manager:
and delegates disable and isDisabled calls to the disabler:
Finally, we have to configure caching and replace DefaultWebSecurityManager in Shiro.ini file:
private final Disabler disabler; @Override public void afterCacheManagerSet() { super.afterCacheManagerSet(); applyCacheManagerToOwnedObjects(); } private void applyCacheManagerToOwnedObjects() { if (disabler instanceof CacheManagerAware) { ((CacheManagerAware) disabler).setCacheManager(getCacheManager()); } }
and delegates disable and isDisabled calls to the disabler:
private final Disabler disabler; @Override public void disable(Subject subject) { disabler.disable(subject); } @Override public boolean isDisabled(Subject subject) { return disabler.isDisabled(subject); }
Finally, we have to configure caching and replace DefaultWebSecurityManager in Shiro.ini file:
# accounts disabling # replace security manager and configure cache securityManager=org.meri.simpleshirosecuredapplication.intrusiondetection.integration.DisablingWebSecurityManager cacheManager=org.apache.shiro.cache.MemoryConstrainedCacheManager securityManager.cacheManager = $cacheManager
AppSensor Utilities
Now, we are ready to add custom ShiroASUtilities to the project:
public class ShiroASUtilities implements ASUtilities { private final IntrusionDetectionLogger logger = new IntrusionDetectionLogger(); @Override public ASUser getCurrentUser() { //acquire logged user from Shiro framework Subject subject = SecurityUtils.getSubject(); //create and return ASUser delegating to Shiro Subject return new ShiroASUser(subject); } @Override public ASLogger getLogger(String className) { return logger; } public HttpServletRequest getCurrentRequest() { HttpServletRequest request = AppSensorIntegrationThreadContext.getCurrentRequest(); return request; } }
We configure AppSensor to use Shiro based classes in appsensor.properties file. Find the line beginning with AppSensor.asUtilities= and replace it with:
# This is the class that handles the utility retriever # replace original line with custom implementation AppSensor.asUtilities = org.meri.simpleshirosecuredapplication.intrusiondetection. integration.ShiroASUtilities
AppSensor is now ready to be used in the application.
1 comments:
Excellent set of articles - I really enjoyed - this could be a very useful addition to the ESAPI and AppSensor projects. Look at Chris Schmidt's Spring Security Authenticator as an example of the new component model ESAPI plans to support. Supporting Shiro would be excellent there as well as for AppSensor. Nice work!
Post a Comment