bigbluebutton-Github/bigbluebutton-web/plugins/jsecurity-0.3/JsecurityGrailsPlugin.groovy
Richard Alam 98dc0bc272 - bringing bigbluebutton-web into trunk
git-svn-id: http://bigbluebutton.googlecode.com/svn/trunk@1190 af16638f-c34d-0410-8cfa-b39d5352b314
2009-04-01 16:53:45 +00:00

554 lines
23 KiB
Groovy

/*
* Copyright 2007 Peter Ledbrook.
*
* 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.
*/
import org.codehaus.groovy.grails.commons.ControllerArtefactHandler
import org.codehaus.groovy.grails.commons.GrailsClassUtils
import org.codehaus.groovy.grails.plugins.support.GrailsPluginUtils
import org.codehaus.groovy.grails.plugins.web.filters.FilterConfig
import org.jsecurity.SecurityUtils
import org.jsecurity.authc.credential.Sha1CredentialsMatcher
import org.jsecurity.grails.*
import org.jsecurity.realm.Realm
import org.jsecurity.subject.DelegatingSubject
import org.jsecurity.web.DefaultWebSecurityManager
import org.springframework.beans.factory.config.MethodInvokingFactoryBean
class JsecurityGrailsPlugin {
def version = '0.3'
def author = 'Peter Ledbrook'
def authorEmail = 'peter@cacoethes.co.uk'
def title = 'Security support via the JSecurity framework.'
def description = '''\
Adds a security layer to your Grails application, allowing you to
protect pages based on a user's roles and/or permissions.
'''
def documentation = 'http://grails.codehaus.org/JSecurity+Plugin'
def grailsVersion = GrailsPluginUtils.grailsVersion
def loadAfter = [ 'controllers', 'services' ]
def observe = [ 'controllers' ]
def watchedResources = 'file:./grails-app/realms/**/*Realm.groovy'
def artefacts = [ RealmArtefactHandler ]
def roleMaps = [:]
def permMaps = [:]
def doWithSpring = {
// Configure the realms defined in the project.
def realmBeans = []
def realmClasses = application.realmClasses
application.realmClasses.each { realmClass ->
log.info "Registering realm: ${realmClass.fullName}"
configureRealm.delegate = delegate
realmBeans << configureRealm(realmClass)
}
// This currently does not work in certain cases because of
// a bug in Grails.
// 'org.jsecurity.spring.LifecycleBeanPostProcessor'(org.jsecurity.spring.LifecycleBeanPostProcessor)
// The default credential matcher.
credentialMatcher(Sha1CredentialsMatcher) {
storedCredentialsHexEncoded = true
}
jsecSecurityManager(DefaultWebSecurityManager) { bean ->
bean.destroyMethod = "destroy"
realms = realmBeans.collect { ref(it) }
// Allow the user to customise the session type: either
// 'http' or 'jsecurity'.
if (application.config.jsecurity.session.mode) {
sessionMode = application.config.jsecurity.session.mode
}
// Allow the user to customise the authentication strategy.
if (application.config.jsecurity.authentication.strategy) {
modularAuthenticationStrategy = application.config.jsecurity.authentication.strategy
}
}
}
def doWithApplicationContext = { applicationContext ->
// Add any extra realms that might have been defined in the
// project. To do that, we first just get all the beans that
// implement the Realm interface.
def beans = applicationContext.getBeanNamesForType(Realm) as List
// Now filter out the beans created by the plugin for the
// realm artefacts.
beans = beans.findAll { !(it.endsWith("Wrapper") || it.endsWith("Proxy")) }
// Finally add the remaining beans to the security manager.
log.info "Registering native realms: $beans"
def mgr = applicationContext.getBean('jsecSecurityManager')
mgr.realms.addAll(beans.collect { applicationContext.getBean(it) })
}
/**
* Adds 'roleMap' and 'permissionMap' properties to controllers
* so that our before-interceptor can query them to find out
* whether a user has the required role/permission for an action.
*/
def doWithDynamicMethods = { ctx ->
// Get the access control information from the controllers, if
// there are any.
if (manager?.hasGrailsPlugin("controllers")) {
// Process each controller.
application.controllerClasses.each { controllerClass ->
processController(controllerClass, log)
}
}
application.filtersClasses.each { filterClass ->
filterClass.clazz.metaClass.getRoleMap = { String controller -> return roleMaps[controller] }
filterClass.clazz.metaClass.getPermissionMap = { String controller -> return permMaps[controller] }
}
// Get the config option that determines whether authentication
// is required for access control or not. By default, it is
// required.
boolean authcRequired = true
if (application.config.jsecurity.authc.required instanceof Boolean) {
authcRequired = application.config.jsecurity.authc.required
}
// Add an 'accessControl' method to FilterConfig (so that it's
// available from Grails filters).
def mc = FilterConfig.metaClass
mc.accessControl << { -> return accessControlMethod(delegate, authcRequired) }
mc.accessControl << { Map args -> return accessControlMethod(delegate, authcRequired, args) }
mc.accessControl << { Closure c -> return accessControlMethod(delegate, authcRequired, [:], c) }
mc.accessControl << { Map args, Closure c -> return accessControlMethod(delegate, authcRequired, args, c) }
}
def doWithWebDescriptor = { webXml ->
def contextParam = webXml.'context-param'
contextParam[contextParam.size() - 1] + {
'filter' {
'filter-name'('securityContextFilter')
'filter-class'('org.jsecurity.spring.SpringJSecurityFilter')
'init-param' {
'param-name'('securityManagerBeanName')
'param-value'('jsecSecurityManager')
}
// If a JSecurity configuration is available, add it
// as an 'init-param' of the filter. This config should
// be in .ini format. See the JSecurity documentation
// for more information.
if (application.config.jsecurity.filter.config) {
'init-param' {
'param-name'('config')
'param-value'(application.config.jsecurity.filter.config)
}
}
}
}
// Place the JSecurity filter after the Spring character encoding
// filter, otherwise the latter filter won't work.
def filter = webXml.'filter-mapping'.find { it.'filter-name'.text() == "charEncodingFilter" }
// NOTE: The following shenanigans are designed to ensure that
// the filter mapping is inserted in the right location under
// a variety of circumstances. However, at this point in time
// it's a bit of wasted effort because Grails itself can't handle
// certain situations, such as no filter mappings at all, or
// a SiteMesh one but no character encoding filter mapping.
// Bleh.
if (!filter) {
// Of course, if there is no char encoding filter, the next
// requirement is that we come before the SiteMesh filter.
// This is trickier to accomplish. First we find out at what
// index the SiteMesh filter mapping is.
int i = 0
int siteMeshIndex = -1
webXml.'filter-mapping'.each {
if (it.'filter-name'.text().equalsIgnoreCase("sitemesh")) {
siteMeshIndex = i
}
i++
}
if (siteMeshIndex > 0) {
// There is at least one other filter mapping that comes
// before the SiteMesh one, so we can simply use the filter
// mapping that comes immediately before SiteMesh as the
// insertion point.
filter = webXml.'filter-mapping'[siteMeshIndex - 1]
}
else if (siteMeshIndex == 0 || webXml.'filter-mapping'.size() == 0) {
// If the index of the SiteMesh filter mapping is 0, i.e.
// it's the first one, we need to use the last filter
// definition as the insertion point. We also need to do
// this if there are no filter mappings.
def filters = webXml.'filter'
filter = filters[filters.size() - 1]
}
else {
// Simply add this filter mapping to the end.
def filterMappings = webXml.'filter-mapping'
filter = filterMappings[filterMappings.size() - 1]
}
}
// Finally add the JSecurity filter mapping after the selected
// insertion point.
filter + {
'filter-mapping' {
'filter-name'('securityContextFilter')
'url-pattern'("/*")
}
}
}
def onChange = { event ->
if (application.isControllerClass(event.source)) {
// Get the GrailsClass instance for the controller.
def controllerClass = application.getControllerClass(event.source?.name)
// If no GrailsClass can be found, i.e. 'controllerClass'
// is null, then this is a new controller.
if (controllerClass == null) {
controllerClass = application.addArtefact(ControllerArtefactHandler.TYPE, event.source)
}
// Now update the role and permission information for this
// controller.
log.info "Reconfiguring access control for ${controllerClass.shortName}"
processController(controllerClass, log)
return
}
else if (application.isRealmClass(event.source)) {
log.info "Realm modified!"
def context = event.ctx
if (!context) {
log.debug("Application context not found - can't reload.")
return
}
boolean isNew = event.application.getRealmClass(event.source?.name) == null
def realmClass = application.addArtefact(RealmArtefactHandler.TYPE, event.source)
if (isNew) {
try {
def beanDefinitions = beans(configureRealm.curry(realmClass))
beanDefinitions.registerBeans(context)
}
catch (MissingMethodException ex) {
// This version of Grails does not support this.
log.warn("Unable to register beans (Grails version < 0.5.5)")
}
}
else {
def realmName = realmClass.shortName
def wrapperName = "${realmName}Wrapper".toString()
def beans = beans {
"${realmName}Class"(MethodInvokingFactoryBean) {
targetObject = ref('grailsApplication', true)
targetMethod = 'getArtefact'
arguments = [RealmArtefactHandler.TYPE, realmClass.fullName]
}
"${realmName}Instance"(ref("${realmName}Class")) {bean ->
bean.factoryMethod = 'newInstance'
bean.singleton = true
bean.autowire = 'byName'
}
"${wrapperName}"(RealmWrapper) {
realm = ref("${realmName}Instance")
tokenClass = GrailsClassUtils.getStaticPropertyValue(realmClass.clazz, 'authTokenClass')
}
}
if (context) {
context.registerBeanDefinition("${realmName}Class", beans.getBeanDefinition("${realmName}Class"))
context.registerBeanDefinition("${realmName}Instance", beans.getBeanDefinition("${realmName}Instance"))
context.registerBeanDefinition(wrapperName, beans.getBeanDefinition(wrapperName))
}
// HACK
//
// The problem here is that the subject has been created
// within a servlet filter *before* the realm reloading
// has occurred. The above 'registerBeanDefinition()'
// calls result in the security manager being destroyed
// and a new one created, but the subject still refers
// to the old security manager.
//
// So, we update the subject's security manager directly.
// Note that we are using Groovy's ability to circumvent
// visibility controls since the 'securityManager' field
// is protected, not public.
if (SecurityUtils.subject instanceof DelegatingSubject) {
SecurityUtils.subject.@securityManager = context.getBean("jsecSecurityManager")
}
}
}
}
def onApplicationChange = { event ->
// TODO Implement code that is executed when any class in a GrailsApplication changes
// the event contain: event.source, event.application and event.applicationContext objects
}
def configureRealm = { grailsClass ->
def realmName = grailsClass.shortName
// Create the realm bean.
"${realmName}Class"(MethodInvokingFactoryBean) {
targetObject = ref('grailsApplication', true)
targetMethod = 'getArtefact'
arguments = [RealmArtefactHandler.TYPE, grailsClass.fullName]
}
"${realmName}Instance"(ref("${realmName}Class")) {bean ->
bean.factoryMethod = 'newInstance'
bean.singleton = true
bean.autowire = 'byName'
}
// Wrap each realm with an adapter that implements the
// JSecurity Realm interface.
def wrapperName = "${realmName}Wrapper".toString()
"${wrapperName}"(RealmWrapper) {
realm = ref("${realmName}Instance")
tokenClass = GrailsClassUtils.getStaticPropertyValue(grailsClass.clazz, 'authTokenClass')
}
// Return the bean name for this realm.
return wrapperName
}
/**
* Implementation of the "accessControl()" dynamic method available
* to filters. It requires a reference to the filter so that it can
* access the dynamic properties and methods, such as "request" and
* "redirect()".
* @param filter The filter instance that the "accessControl()"
* method is called from.
* @param authcRequired Specifies whether the default behaviour is
* to only allow access if the user is authenticated. If
* <code>false</code>, remembered users are also allowed unless this
* setting is overridden in the arguments of the method.
* @param args An argument map as passed to the "accessControl()"
* method. Only the "auth" argument is supported at the moment.
* @param c The closure to execute if the user has not been blocked
* by the authentication requirement. The closure should return
* <code>true</code> to allow access, or <code>false</code> otherwise.
*/
boolean accessControlMethod(filter, boolean authcRequired, Map args = [:], Closure c = null) {
// If we're accessing the auth controller itself, we don't
// want to check whether the user is authenticated, otherwise
// we end up in an infinite loop of redirects.
if (filter.controllerName == 'auth') return true
// Get hold of the filters class instance.
def filtersClass = filter.filtersDefinition
// ...and the HTTP request.
def request = filter.request
// Is an authenticated user required for this URL? If not,
// then we can do a permission check for remembered users
// as well as authenticated ones. Otherwise, remembered
// users will have to log in.
def authenticatedUserRequired = args["auth"] || (args["auth"] == null && authcRequired)
// If required, check that the user is authenticated.
def subject = SecurityUtils.subject
if (subject.principal == null || (authenticatedUserRequired && !subject.authenticated)) {
// User is not authenticated, so deal with it.
if (filtersClass.metaClass.respondsTo(filtersClass, 'onNotAuthenticated')) {
filtersClass.onNotAuthenticated(subject, filter)
}
else {
// Default behaviour is to redirect to the login
// page.
def targetUri = request.forwardURI - request.contextPath
def query = request.queryString
if (query) {
if (!query.startsWith('?')) {
query = '?' + query
}
targetUri += query
}
filter.redirect(
controller: 'auth',
action: 'login',
params: [ targetUri: targetUri ])
}
return false
}
def isPermitted
if (c == null) {
// Check that the user has the required permission for
// the target controller/action. This is access control
// by convention.
isPermitted = subject.isPermitted(new JsecBasicPermission(filter.controllerName, filter.actionName ?: 'index'))
}
else {
// Call the closure with the access control builder and
// check the result. The closure will return 'true' if the
// user is permitted access, otherwise 'false'.
c.delegate = new FilterAccessControlBuilder(subject)
isPermitted = c.call()
}
if (!isPermitted) {
// User does not have the required permission(s)
if (filtersClass.metaClass.respondsTo(filtersClass, 'onUnauthorized')) {
filtersClass.onUnauthorized(subject, filter)
}
else {
// Default behaviour is to redirect to the
// 'unauthorized' page.
filter.redirect(controller: 'auth', action: 'unauthorized')
}
return false
}
else {
return true
}
}
def processController(controllerClass, log) {
// This is the wrapped class.
def clazz = controllerClass.clazz
// These maps are made available to controllers via the
// dynamically injected 'roleMap' and 'permissionMap'
// properties.
def roleMap = [:]
def permissionMap = [:]
this.roleMaps[controllerClass.logicalPropertyName] = roleMap
this.permMaps[controllerClass.logicalPropertyName] = permissionMap
// Process any annotations that this controller declares.
try {
// Check whether the JVM supports annotations.
Class.forName('java.lang.annotation.Annotation')
// Process any annotations on this controller.
log.debug "Processing annotations on ${controllerClass.shortName}"
processAnnotations(controllerClass, roleMap, permissionMap, log)
}
catch (ClassNotFoundException ex) {
}
// Check whether this controller class has a static
// 'accessControl' property. If so, use that as a definition
// of the controller's role and permission requirements.
// Note that these settings override any annotations that
// are declared in the class.
if (GrailsClassUtils.isStaticProperty(clazz, 'accessControl')) {
// The property should be a Closure. If it's not, we
// can't do anything with it.
def c = GrailsClassUtils.getStaticPropertyValue(clazz, 'accessControl')
if (!(c instanceof Closure)) {
log.error("Static property [accessControl] on controller [${controllerClass.fullName}] is not a closure.")
return
}
// Process the closure, building a map of actions to
// permissions and a map of actions to roles.
def b = new AccessControlBuilder(clazz)
c.delegate = b
c.call()
roleMap.putAll(b.roleMap)
permissionMap.putAll(b.permissionMap)
if (log.isDebugEnabled()) {
log.debug("Access control role map for controller '${controllerClass.logicalPropertyName}': ${roleMap}")
log.debug("Access control permission map for controller '${controllerClass.logicalPropertyName}': ${permissionMap}")
}
}
// Inject the role and permission maps into the controller.
controllerClass.metaClass.getRoleMap = {->
return roleMap
}
controllerClass.metaClass.getPermissionMap = {->
return permissionMap
}
}
/**
* Process any plugin annotations (RoleRequired or PermissionRequired)
* on the given controller. Any annotations are evaluated and used
* to update the role and permission maps.
*/
def processAnnotations(controllerClass, roleMap, permissionMap, log) {
def clazz = controllerClass.clazz
clazz.declaredFields.each { field ->
// First see whether this field/action requires any roles.
// We load the annotation classes dynamically so that the
// plugin can be used with the 1.4 JDK.
def ann = field.getAnnotation(Class.forName('org.jsecurity.grails.annotation.RoleRequired'))
if (ann != null) {
if (log.isDebugEnabled()) {
log.debug("Annotation role required by controller '${controllerClass.logicalPropertyName}', action '${field.name}': ${ann.value()}")
}
// Found RoleRequired annotation. Configure the
// interceptor for this.
def roles = roleMap[field.name]
if (!roles) {
roles = []
roleMap[field.name] = roles
}
roles << ann.value()
}
// Now check for permission requirements.
ann = field.getAnnotation(Class.forName('org.jsecurity.grails.annotation.PermissionRequired'))
if (ann != null) {
if (log.isDebugEnabled()) {
log.debug("Annotation permission required by controller '${controllerClass.logicalPropertyName}', action '${field.name}': ${ann.value()}")
}
// Found PermissionRequired annotation. Configure
// the interceptor for this.
def permissions = permissionMap[field.name]
if (!permissions) {
permissions = []
permissionMap[field.name] = permissions
}
def constructor = ann.type().getConstructor([ String, String ] as Class[])
permissions << constructor.newInstance([ ann.target(), ann.actions() ] as Object[])
}
}
}
}