/* * 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. * * * * Modified 2009 Bradley Beddoes, Intient Pty Ltd, Ported to Apache Ki * */ 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.apache.ki.SecurityUtils import org.apache.ki.authc.credential.Sha1CredentialsMatcher import org.apache.ki.grails.* import org.apache.ki.realm.Realm import org.apache.ki.subject.DelegatingSubject import org.apache.ki.web.DefaultWebSecurityManager import org.springframework.beans.factory.config.MethodInvokingFactoryBean class KiGrailsPlugin { // the plugin version def version = "0.1-SNAPSHOT" // the version or versions of Grails the plugin is designed for def grailsVersion = "1.1 > *" // the other plugins this plugin depends on def dependsOn = [:] // resources that are excluded from plugin packaging def pluginExcludes = [ "grails-app/views/error.gsp" ] def author = "Peter Ledbrook, Bradley Beddoes" def authorEmail = "" def title = "Apache Ki Integration for Grails" def description = '''\\ Enables Grails applications to take advantage of the Apache Ki security layer. Adopted from previous JSecurity plugin. ''' // URL to the plugin's documentation def documentation = "http://grails.org/Ki+Plugin" def observe = [ 'controllers' ] def watchedResources = 'file:./grails-app/realms/**/*Realm.groovy' def artefacts = [ RealmArtefactHandler ] def roleMaps = [:] def permMaps = [:] def doWithSpring = { // Configure 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) } credentialMatcher(Sha1CredentialsMatcher) { storedCredentialsHexEncoded = true } kiSecurityManager(DefaultWebSecurityManager) { bean -> realms = realmBeans.collect { ref(it) } // Allow the user to customise the session type: 'http' or 'ki'. if (application.config.security.ki.session.mode) { sessionMode = application.config.security.ki.session.mode } // Allow the user to customise the authentication strategy. if (application.config.security.ki.authentication.strategy) { modularAuthenticationStrategy = application.config.security.ki.authentication.strategy } } lifecycleBeanPostProcessor(org.apache.ki.spring.LifecycleBeanPostProcessor) defaultAdvisorAutoProxyCreator(org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator) { bean -> bean.dependsOn = "lifecycleBeanPostProcessor" } authorizationAttributeSourceAdvisor(org.apache.ki.spring.security.interceptor.AuthorizationAttributeSourceAdvisor) {bean -> securityManager = kiSecurityManager } } def doWithApplicationContext = { applicationContext -> // Add any extra realms that might have been defined in the project def beans = applicationContext.getBeanNamesForType(Realm) as List // Filter out beans created by the plugin for the realm artefacts. beans = beans.findAll { !(it.endsWith("Wrapper") || it.endsWith("Proxy")) } // Add the remaining beans to the security manager. log.info "Registering native realms: $beans" def mgr = applicationContext.getBean('kiSecurityManager') mgr.realms.addAll(beans.collect { applicationContext.getBean(it) }) } def doWithWebDescriptor = { xml -> def contextParam = xml.'context-param' contextParam[contextParam.size() - 1] + { 'filter' { 'filter-name'('securityContextFilter') 'filter-class'('org.apache.ki.spring.SpringKiFilter') 'init-param' { 'param-name'('securityManagerBeanName') 'param-value'('kiSecurityManager') } /* If a Ki configuration is available, add it as an 'init-param' of the filter. This config should be in .ini format. */ if (application.config.security.ki.filter.config) { 'init-param' { 'param-name'('config') 'param-value'(application.config.security.ki.filter.config) } } } } // Place the Ki filter after the Spring character encoding filter, otherwise the latter filter won't work. def filter = xml.'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 xml.'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 = xml.'filter-mapping'[siteMeshIndex - 1] } else if (siteMeshIndex == 0 || xml.'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 = xml.'filter' filter = filters[filters.size() - 1] } else { // Simply add this filter mapping to the end. def filterMappings = xml.'filter-mapping' filter = filterMappings[filterMappings.size() - 1] } } // Finally add the Ki filter mapping after the selected insertion point. filter + { 'filter-mapping' { 'filter-name'('securityContextFilter') 'url-pattern'("/*") } } } /** * Adds 'roleMap' and 'permissionMap' properties to controllers * so that the before-interceptor can query them to find out * whether a user has the required role/permission for an action. */ def doWithDynamicMethods = { ctx -> if (manager?.hasGrailsPlugin("controllers")) { 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.security.ki.authc.required instanceof Boolean) { authcRequired = application.config.security.ki.authc.required } // Add an 'accessControl' method to FilterConfig 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 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) { 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) { def mgr = applicationContext.getBean('kiSecurityManager') SecurityUtils.subject.@securityManager = mgr } } } def onConfigChange = { event -> } 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 Ki 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 * false, 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 * true to allow access, or false 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. isPermitted = subject.isPermitted(new KiBasicPermission(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.apache.ki.grails.annotations.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 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.apache.ki.grails.annotations.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[]) } } } }