import grails.util.GrailsUtil

import org.codehaus.groovy.grails.commons.ControllerArtefactHandler

import org.codehaus.groovy.grails.plugins.springsecurity.AnnotationFilterInvocationDefinition
import org.codehaus.groovy.grails.plugins.springsecurity.AuthenticatedVetoableDecisionManager
import org.codehaus.groovy.grails.plugins.springsecurity.AuthorizeTools
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsAccessDeniedHandlerImpl
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsAuthenticationProcessingFilter
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsDaoAuthenticationProvider
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsDaoImpl
import org.codehaus.groovy.grails.plugins.springsecurity.IpAddressFilter
import org.codehaus.groovy.grails.plugins.springsecurity.LogoutFilterFactoryBean
import org.codehaus.groovy.grails.plugins.springsecurity.QuietMethodSecurityInterceptor
import org.codehaus.groovy.grails.plugins.springsecurity.RequestmapFilterInvocationDefinition
import org.codehaus.groovy.grails.plugins.springsecurity.Secured as SecuredController
import org.codehaus.groovy.grails.plugins.springsecurity.SecurityAnnotationAttributes
import org.codehaus.groovy.grails.plugins.springsecurity.SecurityEventListener
import org.codehaus.groovy.grails.plugins.springsecurity.WithAjaxAuthenticationProcessingFilterEntryPoint

import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
import org.springframework.beans.factory.config.RuntimeBeanReference
import org.springframework.cache.ehcache.EhCacheFactoryBean
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean
import org.springframework.security.annotation.Secured as SecuredService
import org.springframework.security.context.HttpSessionContextIntegrationFilter
import org.springframework.security.context.SecurityContextHolder as SCH
import org.springframework.security.event.authentication.LoggerListener
import org.springframework.security.intercept.method.MethodDefinitionAttributes
import org.springframework.security.intercept.web.FilterSecurityInterceptor
import org.springframework.security.ui.ExceptionTranslationFilter
import org.springframework.security.ui.basicauth.BasicProcessingFilter
import org.springframework.security.ui.basicauth.BasicProcessingFilterEntryPoint
import org.springframework.security.ui.logout.LogoutHandler
import org.springframework.security.ui.logout.SecurityContextLogoutHandler
import org.springframework.security.ui.rememberme.NullRememberMeServices
import org.springframework.security.ui.rememberme.RememberMeProcessingFilter
import org.springframework.security.ui.rememberme.TokenBasedRememberMeServices
import org.springframework.security.ui.session.HttpSessionEventPublisher
import org.springframework.security.ui.switchuser.SwitchUserProcessingFilter
import org.springframework.security.ui.webapp.AuthenticationProcessingFilter
import org.springframework.security.util.AntUrlPathMatcher
import org.springframework.security.util.PortMapperImpl
import org.springframework.security.util.PortResolverImpl
import org.springframework.security.util.RegexUrlPathMatcher
import org.springframework.security.providers.ProviderManager
import org.springframework.security.providers.anonymous.AnonymousAuthenticationProvider
import org.springframework.security.providers.anonymous.AnonymousProcessingFilter
import org.springframework.security.providers.dao.cache.EhCacheBasedUserCache
import org.springframework.security.providers.dao.cache.NullUserCache
import org.springframework.security.providers.encoding.MessageDigestPasswordEncoder
import org.springframework.security.providers.rememberme.RememberMeAuthenticationProvider
import org.springframework.security.securechannel.ChannelDecisionManagerImpl
import org.springframework.security.securechannel.ChannelProcessingFilter
import org.springframework.security.securechannel.InsecureChannelProcessor
import org.springframework.security.securechannel.RetryWithHttpEntryPoint
import org.springframework.security.securechannel.RetryWithHttpsEntryPoint
import org.springframework.security.securechannel.SecureChannelProcessor
import org.springframework.security.util.FilterChainProxy
import org.springframework.security.util.FilterToBeanProxy
import org.springframework.security.vote.AuthenticatedVoter
import org.springframework.security.vote.RoleVoter
import org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter
import org.springframework.web.filter.DelegatingFilterProxy

/**
 * Grails Spring Security 2.0 Plugin.
 *
 * @author T.Yamamoto
 * @author Haotian Sun
 * @author <a href='mailto:beckwithb@studentsonly.com'>Burt Beckwith</a>
 */
class AcegiGrailsPlugin {

	private static final String DEFINITION_SOURCE_PREFIX =
		'CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON\n' +
		'PATTERN_TYPE_APACHE_ANT\n'

	String version = '0.5.1'
	String author = 'Tsuyoshi Yamamoto'
	String authorEmail = 'tyama@xmldo.jp'
	String title = 'Grails Spring Security 2.0 Plugin'
	String description = 'Plugin to use Grails domain class and secure your applications with Spring Security filters.'
	String documentation = 'http://grails.org/AcegiSecurity+Plugin'
	List observe = ['controllers']
	List loadAfter = ['controllers']
	List watchedResources = [
		'file:./grails-app/controllers/**/*Controller.groovy',
		'file:./plugins/*/grails-app/controllers/**/*Controller.groovy',
		'file:./grails-app/conf/SecurityConfig.groovy'
	]
	Map dependsOn = [:]

	def doWithSpring = {

		def conf = AuthorizeTools.securityConfig.security
		if (!conf || !conf.active) {
			println '[active=false] Spring Security not loaded'
			return
		}

		println 'loading security config ...'

		createRefList.delegate = delegate

		/** springSecurityFilterChain */
		configureFilterChain.delegate = delegate
		configureFilterChain conf

		// OpenID
		if (conf.useOpenId) {
			configureOpenId.delegate = delegate
			configureOpenId conf
		}

		// Facebook Connect
		if (conf.useFacebook) {
			configureFacebook.delegate = delegate
			configureFacebook conf
		}

		// logout
		configureLogout.delegate = delegate
		configureLogout conf

		// Basic Auth
		configureBasicAuth.delegate = delegate
		configureBasicAuth conf

		/** httpSessionContextIntegrationFilter */
		httpSessionContextIntegrationFilter(HttpSessionContextIntegrationFilter)

		/** authenticationProcessingFilter */
		authenticationProcessingFilter(GrailsAuthenticationProcessingFilter) {
			authenticationManager = ref('authenticationManager')
			authenticationFailureUrl = conf.authenticationFailureUrl //'/login/authfail?login_error=1'
			ajaxAuthenticationFailureUrl = conf.ajaxAuthenticationFailureUrl // /login/authfail?ajax=true
			defaultTargetUrl = conf.defaultTargetUrl // '/'
			alwaysUseDefaultTargetUrl = conf.alwaysUseDefaultTargetUrl // false
			filterProcessesUrl = conf.filterProcessesUrl // '/j_spring_security_check'
			rememberMeServices = ref('rememberMeServices')
			authenticateService = ref('authenticateService')
		}

		/** securityContextHolderAwareRequestFilter */
		securityContextHolderAwareRequestFilter(SecurityContextHolderAwareRequestFilter) {
			portResolver = ref('portResolver')
		}

		/** rememberMeProcessingFilter */
		rememberMeProcessingFilter(RememberMeProcessingFilter) {
			authenticationManager = ref('authenticationManager')
			rememberMeServices = ref('rememberMeServices')
		}

		/** rememberMeServices */
		if (conf.useOpenId || conf.useFacebook) {
			// auth is external, so no password, so cookie isn't possible
			rememberMeServices(NullRememberMeServices)
		}
		else {
			rememberMeServices(TokenBasedRememberMeServices) {
				userDetailsService = ref('userDetailsService')
				key = conf.rememberMeKey
				cookieName = conf.cookieName
				alwaysRemember = conf.alwaysRemember
				tokenValiditySeconds = conf.tokenValiditySeconds
				parameter = conf.parameter
			}
		}

		/** anonymousProcessingFilter */
		anonymousProcessingFilter(AnonymousProcessingFilter) {
			key = conf.key // 'foo'
			userAttribute = conf.userAttribute //'anonymousUser,ROLE_ANONYMOUS'
		}

		/** exceptionTranslationFilter */
		exceptionTranslationFilter(ExceptionTranslationFilter) {
			authenticationEntryPoint = ref('authenticationEntryPoint')
			accessDeniedHandler = ref('accessDeniedHandler')
			portResolver = ref('portResolver')
		}
		accessDeniedHandler(GrailsAccessDeniedHandlerImpl) {
			errorPage = conf.errorPage == 'null' ? null : conf.errorPage // '/login/denied' or 403
			ajaxErrorPage = conf.ajaxErrorPage
			portResolver = ref('portResolver')
			if (conf.ajaxHeader) {
				ajaxHeader = conf.ajaxHeader //default: X-Requested-With
			}
		}

		if (!conf.useNtlm) {
			authenticationEntryPoint(WithAjaxAuthenticationProcessingFilterEntryPoint) {
				loginFormUrl = conf.loginFormUrl // '/login/auth'
				forceHttps = conf.forceHttps // 'false'
				ajaxLoginFormUrl = conf.ajaxLoginFormUrl // '/login/authAjax'
				if (conf.ajaxHeader) {
					ajaxHeader = conf.ajaxHeader //default: X-Requested-With
				}
				portMapper = ref('portMapper')
				portResolver = ref('portResolver')
			}
		}

		// voters
		configureVoters.delegate = delegate
		configureVoters conf

		/** filterInvocationInterceptor */
		filterInvocationInterceptor(FilterSecurityInterceptor) {
			authenticationManager = ref('authenticationManager')
			accessDecisionManager = ref('accessDecisionManager')
			if (conf.useControllerAnnotations || conf.useRequestMapDomainClass) {
				objectDefinitionSource = ref('objectDefinitionSource')
			}
			else {
				objectDefinitionSource = conf.requestMapString
			}
		}
		if (conf.useControllerAnnotations) {
			objectDefinitionSource(AnnotationFilterInvocationDefinition) {
				boolean lowercase = conf.controllerAnnotationsMatchesLowercase
				if ('ant'.equals(conf.controllerAnnotationsMatcher)) {
					urlMatcher = new AntUrlPathMatcher(lowercase)
				}
				else {
					urlMatcher = new RegexUrlPathMatcher(lowercase)
				}
				if (conf.controllerAnnotationsRejectIfNoRule instanceof Boolean) {
					rejectIfNoRule = conf.controllerAnnotationsRejectIfNoRule
				}
			}
		}
		else if (conf.useRequestMapDomainClass) {
			objectDefinitionSource(RequestmapFilterInvocationDefinition) {
				requestMapClass = conf.requestMapClass
				requestMapPathFieldName = conf.requestMapPathField
				requestMapConfigAttributeField = conf.requestMapConfigAttributeField
				urlMatcher = new AntUrlPathMatcher(true)
			}
		}

		/** anonymousAuthenticationProvider */
		anonymousAuthenticationProvider(AnonymousAuthenticationProvider) {
			key = conf.key // 'foo'
		}
		/** rememberMeAuthenticationProvider */
		rememberMeAuthenticationProvider(RememberMeAuthenticationProvider) {
			key = conf.rememberMeKey
		}

		// authenticationManager
		configureAuthenticationManager.delegate = delegate
		configureAuthenticationManager conf

		/** daoAuthenticationProvider */
		daoAuthenticationProvider(GrailsDaoAuthenticationProvider) {
			userDetailsService = ref('userDetailsService')
			passwordEncoder = ref('passwordEncoder')
			userCache = ref('userCache')
		}

		/** passwordEncoder */
		passwordEncoder(MessageDigestPasswordEncoder, conf.algorithm) {
			if (conf.encodeHashAsBase64) {
				encodeHashAsBase64 = true
			}
		}

		// user details cache
		if (conf.cacheUsers) {
			userCache(EhCacheBasedUserCache) {
				cache = ref('securityUserCache')
			}
			securityUserCache(EhCacheFactoryBean) {
				cacheManager = ref('cacheManager')
				cacheName = 'userCache'
			}
			cacheManager(EhCacheManagerFactoryBean)
		}
		else {
			userCache(NullUserCache)
		}

		/** userDetailsService */
		userDetailsService(GrailsDaoImpl) {
			usernameFieldName = conf.userName
			passwordFieldName = conf.password
			enabledFieldName = conf.enabled
			authorityFieldName = conf.authorityField
			loginUserDomainClass = conf.loginUserDomainClass
			relationalAuthoritiesField = conf.relationalAuthorities
			authoritiesMethodName = conf.getAuthoritiesMethod
			sessionFactory = ref('sessionFactory')
			authenticateService = ref('authenticateService')
		}

		/** loggerListener ( log4j.logger.org.springframework.security=info,stdout ) */
		if (conf.useLogger) {
			loggerListener(LoggerListener)
		}

		// port mappings for channel security, etc.
		portMapper(PortMapperImpl) {
			portMappings = [(conf.httpPort.toString()) : conf.httpsPort.toString()]
		}
		portResolver(PortResolverImpl) {
			portMapper = portMapper
		}

		daacc(DefaultAdvisorAutoProxyCreator)

		// experiment on Annotation and MethodSecurityInterceptor for secure services
		configureAnnotatedServices.delegate = delegate
		configureAnnotatedServices conf

		// simple email service
		configureMail.delegate = delegate
		configureMail conf

		// Switch User
		if (conf.switchUserProcessingFilter) {
			switchUserProcessingFilter(SwitchUserProcessingFilter) {
				userDetailsService = ref('userDetailsService')
				switchUserUrl = conf.swswitchUserUrl
				exitUserUrl = conf.swexitUserUrl
				targetUrl = conf.swtargetUrl
			}
		}

		// LDAP
		if (conf.useLdap) {
			configureLdap.delegate = delegate
			configureLdap conf
		}

		// SecurityEventListener
		securityEventListener(SecurityEventListener) {
			authenticateService = ref('authenticateService')
		}

		// Kerberos
		if (conf.useKerberos) {
			configureKerberos.delegate = delegate
			configureKerberos conf
		}

		// NTLM
		if (conf.useNtlm) {
			configureNtlm.delegate = delegate
			configureNtlm conf
		}

		// CAS
		if (conf.useCAS) {
			configureCAS.delegate = delegate
			configureCAS conf
		}

		// channel (http/https) security
		if (useSecureChannel(conf)) {
			configureChannelProcessingFilter.delegate = delegate
			configureChannelProcessingFilter conf
		}

		// IP filter
		if (conf.ipRestrictions) {
			configureIpFilter.delegate = delegate
			configureIpFilter conf
		}
	}

	private boolean useSecureChannel(conf) {
		return conf.secureChannelDefinitionSource || conf.channelConfig.secure || conf.channelConfig.insecure
	}

	// OpenID
	private def configureOpenId = { conf ->
		openIDAuthProvider(org.codehaus.groovy.grails.plugins.springsecurity.openid.GrailsOpenIdAuthenticationProvider) {
			userDetailsService = ref('userDetailsService')
		}
		openIDStore(org.openid4java.consumer.InMemoryConsumerAssociationStore)
		openIDNonceVerifier(org.openid4java.consumer.InMemoryNonceVerifier, conf.openIdNonceMaxSeconds) // 300 seconds
		openIDConsumerManager(org.openid4java.consumer.ConsumerManager) {
			nonceVerifier = openIDNonceVerifier
		}
		openIDConsumer(org.springframework.security.ui.openid.consumers.OpenID4JavaConsumer, openIDConsumerManager)
		openIDAuthenticationProcessingFilter(org.springframework.security.ui.openid.OpenIDAuthenticationProcessingFilter) {
			authenticationManager = ref('authenticationManager')
			authenticationFailureUrl = conf.authenticationFailureUrl //'/login/authfail?login_error=1' // /spring_security_login?login_error
			defaultTargetUrl = conf.defaultTargetUrl // '/'
			filterProcessesUrl = '/j_spring_openid_security_check' // not configurable
			rememberMeServices = ref('rememberMeServices')
			consumer = openIDConsumer
		}
	}

	// Facebook
	private def configureFacebook = { conf ->
		facebookAuthProvider(org.codehaus.groovy.grails.plugins.springsecurity.facebook.FacebookAuthenticationProvider) {
			userDetailsService = ref('userDetailsService')
		}
		facebookAuthenticationProcessingFilter(org.codehaus.groovy.grails.plugins.springsecurity.facebook.FacebookAuthenticationProcessingFilter) {
			authenticationManager = ref('authenticationManager')
			authenticationFailureUrl = conf.authenticationFailureUrl //'/login/authfail?login_error=1' // /spring_security_login?login_error
			defaultTargetUrl = conf.defaultTargetUrl // '/'
			filterProcessesUrl = conf.facebook.filterProcessesUrl // '/j_spring_facebook_security_check'
			apiKey = conf.facebook.apiKey
			secretKey = conf.facebook.secretKey
			authenticationUrlRoot = conf.facebook.authenticationUrlRoot // http://www.facebook.com/login.php?v=1.0&api_key=
			rememberMeServices = ref('rememberMeServices')
		}
		facebookLogoutHandler(org.codehaus.groovy.grails.plugins.springsecurity.facebook.FacebookLogoutHandler) {
			apiKey = conf.facebook.apiKey
		}
	}

	private def configureCAS = { conf ->
		String casHost = conf.cas.casServer ?: 'localhost'
		int casPort = (conf.cas.casServerPort ?: '443').toInteger()
		String casFilterProcessesUrl = conf.cas.filterProcessesUrl ?: '/j_spring_cas_security_check'
		boolean sendRenew = Boolean.valueOf(conf.cas.sendRenew ?: false)
		String proxyReceptorUrl = conf.cas.proxyReceptorUrl ?: '/secure/receptor'
		String applicationHost = System.getProperty('server.host') ?: 'localhost'
		int applicationPort = (System.getProperty('server.port') ?: 8080).toInteger()
		String appName = application.metadata['app.name']
		String casHttp = conf.cas.casServerSecure ? 'https' : 'http'
		String localHttp = conf.cas.localhostSecure ? 'https' : 'http'

		proxyGrantingTicketStorage(org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl)

		casProcessingFilter(org.springframework.security.ui.cas.CasProcessingFilter) {
			authenticationManager = ref('authenticationManager')
			authenticationFailureUrl = conf.cas.failureURL ?: '/denied.jsp'
			defaultTargetUrl = conf.cas.defaultTargetURL ?: '/'
			filterProcessesUrl = casFilterProcessesUrl
			proxyGrantingTicketStorage = proxyGrantingTicketStorage
			proxyReceptorUrl = proxyReceptorUrl
		}

		casServiceProperties(org.springframework.security.ui.cas.ServiceProperties) {
			service = "$localHttp://$applicationHost:$applicationPort/$appName$casFilterProcessesUrl"
			sendRenew = sendRenew
		}

		String casLoginURL = conf.cas.fullLoginURL ?: "$casHttp://$casHost:$casPort/cas/login"
		authenticationEntryPoint(org.springframework.security.ui.cas.CasProcessingFilterEntryPoint) {
			loginUrl = casLoginURL
			serviceProperties = casServiceProperties
		}

		String casServiceURL = conf.cas.fullServiceURL ?: "$casHttp://$casHost:$casPort/cas"
		cas20ServiceTicketValidator(org.jasig.cas.client.validation.Cas20ServiceTicketValidator, casServiceURL) {
			proxyGrantingTicketStorage = proxyGrantingTicketStorage
			proxyCallbackUrl = "$localHttp://$applicationHost:$applicationPort/$appName$proxyReceptorUrl"
		}

		// the CAS authentication provider key doesn't need to be anything special, it just identifies individual providers
		// so that they can identify tokens it previously authenticated
		String casAuthenticationProviderKey = conf.cas.authenticationProviderKey ?: appName + System.currentTimeMillis()
		casAuthenticationProvider(org.springframework.security.providers.cas.CasAuthenticationProvider) {
			userDetailsService = ref(conf.cas.userDetailsService ?: 'userDetailsService')
			serviceProperties = casServiceProperties
			ticketValidator = cas20ServiceTicketValidator
			key = casAuthenticationProviderKey
		}
	}

	private def configureLogout = { conf ->

		securityContextLogoutHandler(SecurityContextLogoutHandler)
		def logoutHandlerNames = conf.logoutHandlerNames
		if (!logoutHandlerNames) {
			logoutHandlerNames = []
			if (conf.useFacebook) {
				logoutHandlerNames << 'facebookLogoutHandler'
			}
			else if (!conf.useOpenId) {
				logoutHandlerNames << 'rememberMeServices'
			}
			logoutHandlerNames << 'securityContextLogoutHandler'
		}

		def logoutHandlers = createRefList(logoutHandlerNames)
		def afterLogoutUrl = conf.afterLogoutUrl // '/'

		/** logoutFilter */
		logoutFilter(LogoutFilterFactoryBean) {
			logoutSuccessUrl = afterLogoutUrl
			handlers = logoutHandlers
		}
	}

	private def configureBasicAuth = { conf ->

		basicProcessingFilterEntryPoint(BasicProcessingFilterEntryPoint) {
			realmName = conf.realmName // 'Grails Realm'
		}
		basicProcessingFilter(BasicProcessingFilter) {
			authenticationManager = ref('authenticationManager')
			authenticationEntryPoint = basicProcessingFilterEntryPoint
		}
	}

	private def configureVoters = { conf ->

		roleVoter(RoleVoter)

		authenticatedVoter(AuthenticatedVoter)

		def decisionVoterNames = conf.decisionVoterNames
		if (!decisionVoterNames) {
			decisionVoterNames = ['authenticatedVoter', 'roleVoter']
		}
		def decisionVoterList = createRefList(decisionVoterNames)
		/** accessDecisionManager */
		accessDecisionManager(AuthenticatedVetoableDecisionManager) {
			allowIfAllAbstainDecisions = false
			decisionVoters = decisionVoterList
		}
	}

	private def configureAuthenticationManager = { conf ->

		def providerNames = conf.providerNames
		if (!providerNames) {
			providerNames = []
			if (conf.useKerberos) {
				providerNames << 'kerberosAuthProvider'
			}
			if (conf.useCAS) {
				providerNames << 'casAuthenticationProvider'
			}
			if (conf.useLdap) {
				providerNames << 'ldapAuthProvider'
			}
			if (conf.useFacebook) {
				providerNames << 'facebookAuthProvider'
			}

			if (providerNames.empty) {
				providerNames << 'daoAuthenticationProvider'
				if (conf.useOpenId) {
					providerNames << 'openIDAuthProvider'
				}
			}

			providerNames << 'anonymousAuthenticationProvider'
			providerNames << 'rememberMeAuthenticationProvider'
		}

		def providerList = createRefList(providerNames)
		/** authenticationManager */
		authenticationManager(ProviderManager) {
			providers = providerList
		}
	}

	private def configureAnnotatedServices = { conf ->

		serviceSecureAnnotation(SecurityAnnotationAttributes)

		serviceSecureAnnotationODS(MethodDefinitionAttributes) {
			attributes = serviceSecureAnnotation
		}

		/** securityInteceptor */
		securityInteceptor(QuietMethodSecurityInterceptor) {
			validateConfigAttributes = false
			authenticationManager = ref('authenticationManager')
			accessDecisionManager = ref('accessDecisionManager')
			objectDefinitionSource = serviceSecureAnnotationODS
			throwException = true
		}

		// load Services which have Annotations
		application.serviceClasses.each { serviceClass ->
			if (hasAnnotation(serviceClass.clazz)) {
				"${serviceClass.propertyName}Sec"(BeanNameAutoProxyCreator) {
					beanNames = serviceClass.propertyName
					interceptorNames = ['securityInteceptor']
					proxyTargetClass = true
				}
			}
		}
	}

	private def configureMail = { conf ->

		if (conf.useMail) {
			mailSender(org.springframework.mail.javamail.JavaMailSenderImpl) {
				host = conf.mailHost
				username = conf.mailUsername
				password = conf.mailPassword
				protocol = conf.mailProtocol
				port = conf.mailPort
				if (conf.javaMailProperties) {
					javaMailProperties = conf.javaMailProperties as Properties
				}
			}

			mailMessage(org.springframework.mail.SimpleMailMessage) {
				from = conf.mailFrom
			}
		}
	}

	private def configureLdap = { conf ->

		contextSource(org.springframework.security.ldap.DefaultSpringSecurityContextSource, conf.ldapServer) {
			userDn = conf.ldapManagerDn
			password = conf.ldapManagerPassword
		}

		ldapUserSearch(org.springframework.security.ldap.search.FilterBasedLdapUserSearch,
			           conf.ldapSearchBase, conf.ldapSearchFilter, contextSource) {
			searchSubtree = conf.ldapSearchSubtree
		}

		ldapAuthenticator(org.springframework.security.providers.ldap.authenticator.BindAuthenticator,
			              contextSource) {
			userSearch = ldapUserSearch
		}

		ldapUserDetailsMapper(org.codehaus.groovy.grails.plugins.springsecurity.ldap.GrailsLdapUserDetailsMapper) {
			userDetailsService = ref('userDetailsService')
			passwordAttributeName = conf.ldapPasswordAttributeName // 'userPassword'
			usePassword = conf.ldapUsePassword // true
			retrieveDatabaseRoles = conf.ldapRetrieveDatabaseRoles // false
		}

		if (conf.ldapRetrieveGroupRoles) {
			ldapAuthoritiesPopulator(org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator,
				                     contextSource, conf.ldapGroupSearchBase) {
				groupRoleAttribute = conf.ldapGroupRoleAttribute
				groupSearchFilter = conf.ldapGroupSearchFilter
				searchSubtree = conf.ldapSearchSubtree
			}
			ldapAuthProvider(org.springframework.security.providers.ldap.LdapAuthenticationProvider,
				             ldapAuthenticator, ldapAuthoritiesPopulator) {
				userDetailsContextMapper = ldapUserDetailsMapper
			}
		}
		else {
			// use the NullAuthoritiesPopulator
			ldapAuthProvider(org.springframework.security.providers.ldap.LdapAuthenticationProvider,
				             ldapAuthenticator) {
				userDetailsContextMapper = ldapUserDetailsMapper
			}
		}
	}

	private def configureKerberos = { conf ->

		jaasNameCallbackHandler(org.springframework.security.providers.jaas.JaasNameCallbackHandler)

		jaasPasswordCallbackHandler(org.springframework.security.providers.jaas.JaasPasswordCallbackHandler)

		kerberosAuthProvider(org.codehaus.groovy.grails.plugins.springsecurity.kerberos.GrailsKerberosAuthenticationProvider) {
			authenticateService = ref('authenticateService')
			userDetailsService = ref('userDetailsService')
			loginConfig = conf.kerberosLoginConfigFile
			loginContextName = 'KrbAuthentication'
			callbackHandlers = [jaasNameCallbackHandler, jaasPasswordCallbackHandler]
			authorityGranters = []
		}

		//TODO: Improve
		System.setProperty('java.security.krb5.realm', conf.kerberosRealm)
		System.setProperty('java.security.krb5.kdc', conf.kerberosKdc)
	}

	private def configureNtlm = { conf ->

		ntlmFilter(org.springframework.security.ui.ntlm.NtlmProcessingFilter) {
			stripDomain = conf.ntlm.stripDomain // true
			retryOnAuthFailure = conf.ntlm.retryOnAuthFailure // true
			defaultDomain = conf.ntlm.defaultDomain
			netbiosWINS = conf.ntlm.netbiosWINS
			forceIdentification = conf.ntlm.forceIdentification // false
			authenticationManager = ref('authenticationManager')
		}

		authenticationEntryPoint(org.codehaus.groovy.grails.plugins.springsecurity.GrailsNtlmProcessingFilterEntryPoint) {
			authenticationFailureUrl = conf.authenticationFailureUrl
		}
	}

	private def configureFilterChain = { conf ->

		def filterNames = conf.filterNames
		if (!filterNames) {
			filterNames = []

			if (useSecureChannel(conf)) {
				filterNames << 'channelProcessingFilter' // CHANNEL_FILTER
			}

			// CONCURRENT_SESSION_FILTER

			filterNames << 'httpSessionContextIntegrationFilter' // HTTP_SESSION_CONTEXT_FILTER

			filterNames << 'logoutFilter' // LOGOUT_FILTER

			if (conf.ipRestrictions) {
				filterNames << 'ipAddressFilter'
			}

			// X509_FILTER

			// PRE_AUTH_FILTER

			if (conf.useCAS) {
				filterNames << 'casProcessingFilter' // CAS_PROCESSING_FILTER
			}

			filterNames << 'authenticationProcessingFilter' // AUTHENTICATION_PROCESSING_FILTER

			if (conf.useOpenId) {
				filterNames << 'openIDAuthenticationProcessingFilter' // OPENID_PROCESSING_FILTER
			}

			if (conf.useFacebook) {
				filterNames << 'facebookAuthenticationProcessingFilter'
			}

			// LOGIN_PAGE_FILTER

			if (conf.basicProcessingFilter) {
				filterNames << 'basicProcessingFilter' // BASIC_PROCESSING_FILTER
			}

			if (!conf.useNtlm) {
				// seems to remove NTLM authentication tokens
				filterNames << 'securityContextHolderAwareRequestFilter' // SERVLET_API_SUPPORT_FILTER
			}

			filterNames << 'rememberMeProcessingFilter' // REMEMBER_ME_FILTER

			filterNames << 'anonymousProcessingFilter' // ANONYMOUS_FILTER

			filterNames << 'exceptionTranslationFilter' // EXCEPTION_TRANSLATION_FILTER

			if (conf.useNtlm) {
				filterNames << 'ntlmFilter' // NTLM_FILTER
			}

			// SESSION_FIXATION_FILTER

			filterNames << 'filterInvocationInterceptor' // FILTER_SECURITY_INTERCEPTOR

			if (conf.switchUserProcessingFilter) {
				filterNames << 'switchUserProcessingFilter' // SWITCH_USER_FILTER
			}
		}
		String joinedFilters = filterNames.join(',')

		String definitionSource
		if (conf.filterInvocationDefinitionSource) {
			// if the entire string is set in the config, use that
			definitionSource = conf.filterInvocationDefinitionSource
		}
		else if (conf.filterInvocationDefinitionSourceMap) {
			// otherwise if there's a map of configs, use those
			definitionSource = DEFINITION_SOURCE_PREFIX
			conf.filterInvocationDefinitionSourceMap.each { key, value ->
				if (value == 'JOINED_FILTERS') {
					// special case to use either the filters defined by
					// conf.filterNames or the filters defined by config settings
					value = joinedFilters
				}
				definitionSource += "$key=$value\n"
			}
		}
		else {
			// otherwise build the default string - all urls guarded by all filters
			definitionSource = "$DEFINITION_SOURCE_PREFIX\n/**=$joinedFilters"
		}
		springSecurityFilterChain(FilterChainProxy) {
			filterInvocationDefinitionSource = definitionSource
		}
	}

	private def configureChannelProcessingFilter = { conf ->

		retryWithHttpEntryPoint(RetryWithHttpEntryPoint) {
			portMapper = ref('portMapper')
			portResolver = ref('portResolver')
		}

		retryWithHttpsEntryPoint(RetryWithHttpsEntryPoint) {
			portMapper = ref('portMapper')
			portResolver = ref('portResolver')
		}

		secureChannelProcessor(SecureChannelProcessor) {
			entryPoint = retryWithHttpsEntryPoint
		}

		insecureChannelProcessor(InsecureChannelProcessor) {
			entryPoint = retryWithHttpEntryPoint
		}

		channelDecisionManager(ChannelDecisionManagerImpl) {
			channelProcessors = [insecureChannelProcessor, secureChannelProcessor]
		}

		String definitionSource
		if (conf.secureChannelDefinitionSource) {
			// if the entire string is set in the config, use that
			definitionSource = conf.secureChannelDefinitionSource
		}
		else {
			definitionSource = DEFINITION_SOURCE_PREFIX
			conf.channelConfig.secure.each { pattern ->
				definitionSource += "$pattern=REQUIRES_SECURE_CHANNEL\n"
			}
			conf.channelConfig.insecure.each { pattern ->
				definitionSource += "$pattern=REQUIRES_INSECURE_CHANNEL\n"
			}
		}
		channelProcessingFilter(ChannelProcessingFilter) {
			channelDecisionManager = channelDecisionManager
			filterInvocationDefinitionSource = definitionSource
		}
	}

	private def configureIpFilter = { conf ->
		ipAddressFilter(IpAddressFilter) {
			ipRestrictions = conf.ipRestrictions
		}
	}

	def doWithApplicationContext = { ctx ->
		// nothing to do
	}

	def doWithWebDescriptor = { xml ->

		def conf = AuthorizeTools.securityConfig.security
		if (!conf || !conf.active) {
			return
		}

		// we add the filter(s) right after the last context-param
		def contextParam = xml.'context-param'

		// the name of the filter matches the name of the Spring bean that it delegates to
		contextParam[contextParam.size() - 1] + {
			'filter' {
				'filter-name'('springSecurityFilterChain')
				'filter-class'(DelegatingFilterProxy.name)
			}
		}

		// add the filter-mapping after the Spring character encoding filter
		findMappingLocation.delegate = delegate
		def mappingLocation = findMappingLocation(xml)
		mappingLocation + {
			'filter-mapping'{
				'filter-name'('springSecurityFilterChain')
				'url-pattern'('/*')
			}
		}

		if (conf.useHttpSessionEventPublisher) {
			def filterMapping = xml.'filter-mapping'
			filterMapping[filterMapping.size() - 1] + {
				'listener' {
					'listener-class'(HttpSessionEventPublisher.name)
				}
			}
		}
	}

	private def findMappingLocation = { xml ->

		// find the location to insert the filter-mapping; needs to be after the 'charEncodingFilter'
		// which may not exist. should also be before the sitemesh filter.
		// thanks to the JSecurity plugin for the logic.

		def mappingLocation = xml.'filter-mapping'.find { it.'filter-name'.text() == 'charEncodingFilter' }
		if (mappingLocation) {
			return mappingLocation
		}

		// no 'charEncodingFilter'; try to put it before sitemesh
		int i = 0
		int siteMeshIndex = -1
		xml.'filter-mapping'.each {
			if (it.'filter-name'.text().equalsIgnoreCase('sitemesh')) {
				siteMeshIndex = i
			}
			i++
		}
		if (siteMeshIndex > 0) {
			return xml.'filter-mapping'[siteMeshIndex - 1]
		}

		if (siteMeshIndex == 0 || xml.'filter-mapping'.size() == 0) {
			def filters = xml.'filter'
			return filters[filters.size() - 1]
		}

		// neither filter found
		def filters = xml.'filter'
		return filters[filters.size() - 1]
	}

	def doWithDynamicMethods = { ctx ->

		def conf = AuthorizeTools.securityConfig.security
		if (!conf || !conf.active) {
			return
		}

		for (controllerClass in application.controllerClasses) {
			addControllerMethods controllerClass.metaClass
		}

		if (conf.useControllerAnnotations) {
			ctx.objectDefinitionSource.initialize conf.controllerAnnotationStaticRules,
				ctx.grailsUrlMappingsHolder, application.controllerClasses
		}
	}

	def onChange = { event ->

		def conf = AuthorizeTools.securityConfig.security
		if (!conf || !conf.active) {
			return
		}

		def ctx = event.ctx
		if (event.source && ctx && event.application) {
			boolean isControllerClass = application.isControllerClass(event.source)
			boolean configChanged = 'SecurityConfig'.equals(event.source.name)
			if (configChanged || isControllerClass) {
				if (conf.useControllerAnnotations) {
					ctx.objectDefinitionSource.initialize conf.controllerAnnotationStaticRules,
						ctx.grailsUrlMappingsHolder, application.controllerClasses
				}
				if (isControllerClass) {
					addControllerMethods application.getControllerClass(event.source.name).metaClass
				}
			}
		}
	}

	def onApplicationChange = { event ->
		// nothing to do
	}

	private void addControllerMethods(MetaClass mc) {
		mc.getAuthUserDomain = {
			def principal = SCH.context?.authentication?.principal
			if (principal != null && principal != 'anonymousUser') {
				return principal?.domainClass
			}

			return null
		}

		mc.getPrincipalInfo = {
			return SCH.context?.authentication?.principal
		}

		mc.isUserLogon = {
			def principal = SCH.context?.authentication?.principal
			return principal != null && principal != 'anonymousUser'
		}
	}

	private boolean hasAnnotation(serviceClass) {
		for (method in serviceClass.methods) {
			if (method.isAnnotationPresent(SecuredService)) {
				return true
			}
		}

		return false
	}

	private def createRefList = { names ->
		names.collect { name -> ref(name) }
	}
}
