This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch feat/forge-plugin-features in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 259bccdd511fafbc6c1cf0a3a19c54a18c80eae9 Author: James Daugherty <[email protected]> AuthorDate: Sat Jul 18 14:20:26 2026 -0400 Forge features for integrated plugins --- .../SpringResources.java => mail/Mail.java} | 48 +++++--- .../SpringResources.java => redis/Redis.java} | 48 +++++--- .../forge/feature/security/SpringSecurityCore.java | 126 +++++++++++++++++++++ .../security/template/securityRole.rocker.raw | 50 ++++++++ .../security/template/securityUser.rocker.raw | 79 +++++++++++++ .../securityUserPasswordEncoderListener.rocker.raw | 66 +++++++++++ .../security/template/securityUserRole.rocker.raw | 113 ++++++++++++++++++ .../template/springSecurityResources.rocker.raw | 33 ++++++ .../forge/feature/spring/SpringResources.java | 6 +- .../org/grails/forge/feature/mail/MailSpec.groovy | 57 ++++++++++ .../grails/forge/feature/redis/RedisSpec.groovy | 57 ++++++++++ .../feature/security/SpringSecurityCoreSpec.groovy | 98 ++++++++++++++++ 12 files changed, 744 insertions(+), 37 deletions(-) diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/mail/Mail.java similarity index 57% copy from grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java copy to grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/mail/Mail.java index 79eb828524..edde64563f 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/mail/Mail.java @@ -16,49 +16,61 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.forge.feature.spring; +package org.grails.forge.feature.mail; import jakarta.inject.Singleton; import org.grails.forge.application.ApplicationType; import org.grails.forge.application.generator.GeneratorContext; +import org.grails.forge.build.dependencies.Dependency; import org.grails.forge.feature.Category; -import org.grails.forge.feature.DefaultFeature; import org.grails.forge.feature.Feature; -import org.grails.forge.feature.spring.template.springResources; -import org.grails.forge.options.Options; -import org.grails.forge.template.RockerTemplate; +import org.grails.forge.util.VersionInfo; -import java.util.Set; +import java.util.Map; @Singleton -public class SpringResources implements DefaultFeature { - @Override - public boolean shouldApply(ApplicationType applicationType, Options options, Set<Feature> selectedFeatures) { - return true; - } +public class Mail implements Feature { @Override public String getName() { - return "spring-resources"; + return "mail"; } @Override - public boolean supports(ApplicationType applicationType) { - return true; + public String getTitle() { + return "Mail"; } @Override - public boolean isVisible() { - return false; + public String getDescription() { + return "The Mail Plugin provides a mailService and the sendMail builder for sending email, " + + "including GSP-rendered mail bodies."; } @Override public void apply(GeneratorContext generatorContext) { - generatorContext.addTemplate("springResources", new RockerTemplate("grails-app/conf/spring/resources.groovy", springResources.template())); + Map<String, Object> config = generatorContext.getConfiguration(); + config.put("grails.mail.host", "localhost"); + config.put("grails.mail.port", 25); + generatorContext.addDependency(Dependency.builder() + .groupId("org.apache.grails") + .artifactId("grails-mail") + .implementation()); + } + + @Override + public boolean supports(ApplicationType applicationType) { + return true; } @Override public String getCategory() { - return Category.SPRING; + return Category.MESSAGING; } + + @Override + public String getDocumentation() { + return "https://grails.apache.org/docs/" + VersionInfo.getDocumentationVersion() + "/guide/mail.html"; + } + } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/redis/Redis.java similarity index 57% copy from grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java copy to grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/redis/Redis.java index 79eb828524..f1cdc93d21 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/redis/Redis.java @@ -16,49 +16,61 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.forge.feature.spring; +package org.grails.forge.feature.redis; import jakarta.inject.Singleton; import org.grails.forge.application.ApplicationType; import org.grails.forge.application.generator.GeneratorContext; +import org.grails.forge.build.dependencies.Dependency; import org.grails.forge.feature.Category; -import org.grails.forge.feature.DefaultFeature; import org.grails.forge.feature.Feature; -import org.grails.forge.feature.spring.template.springResources; -import org.grails.forge.options.Options; -import org.grails.forge.template.RockerTemplate; +import org.grails.forge.util.VersionInfo; -import java.util.Set; +import java.util.Map; @Singleton -public class SpringResources implements DefaultFeature { - @Override - public boolean shouldApply(ApplicationType applicationType, Options options, Set<Feature> selectedFeatures) { - return true; - } +public class Redis implements Feature { @Override public String getName() { - return "spring-resources"; + return "redis"; } @Override - public boolean supports(ApplicationType applicationType) { - return true; + public String getTitle() { + return "Redis"; } @Override - public boolean isVisible() { - return false; + public String getDescription() { + return "The Redis Plugin provides a redisService and access to Jedis connection pools " + + "for working with a Redis data store."; } @Override public void apply(GeneratorContext generatorContext) { - generatorContext.addTemplate("springResources", new RockerTemplate("grails-app/conf/spring/resources.groovy", springResources.template())); + Map<String, Object> config = generatorContext.getConfiguration(); + config.put("grails.redis.host", "localhost"); + config.put("grails.redis.port", 6379); + generatorContext.addDependency(Dependency.builder() + .groupId("org.apache.grails") + .artifactId("grails-redis") + .implementation()); + } + + @Override + public boolean supports(ApplicationType applicationType) { + return true; } @Override public String getCategory() { - return Category.SPRING; + return Category.DATABASE; } + + @Override + public String getDocumentation() { + return "https://grails.apache.org/docs/" + VersionInfo.getDocumentationVersion() + "/guide/redis.html"; + } + } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/SpringSecurityCore.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/SpringSecurityCore.java new file mode 100644 index 0000000000..f96fadfc6b --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/SpringSecurityCore.java @@ -0,0 +1,126 @@ +/* + * 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 + * + * https://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.grails.forge.feature.security; + +import jakarta.inject.Singleton; +import org.grails.forge.application.ApplicationType; +import org.grails.forge.application.Project; +import org.grails.forge.application.generator.GeneratorContext; +import org.grails.forge.build.dependencies.Dependency; +import org.grails.forge.feature.Category; +import org.grails.forge.feature.Feature; +import org.grails.forge.feature.security.template.securityRole; +import org.grails.forge.feature.security.template.springSecurityResources; +import org.grails.forge.feature.security.template.securityUser; +import org.grails.forge.feature.security.template.securityUserPasswordEncoderListener; +import org.grails.forge.feature.security.template.securityUserRole; +import org.grails.forge.template.RockerTemplate; +import org.grails.forge.util.VersionInfo; + +import java.util.List; +import java.util.Map; + +@Singleton +public class SpringSecurityCore implements Feature { + + @Override + public String getName() { + return "spring-security"; + } + + @Override + public String getTitle() { + return "Spring Security Core"; + } + + @Override + public String getDescription() { + return "Secures the application with the Spring Security Core plugin, generating the SecurityUser, SecurityRole and " + + "SecurityUserRole domain classes and the standard permit-all rules for the public pages " + + "(the same starting point the s2-quickstart command produces)."; + } + + @Override + public void apply(GeneratorContext generatorContext) { + Project project = generatorContext.getProject(); + String packageName = project.getPackageName(); + + Map<String, Object> config = generatorContext.getConfiguration(); + config.put("grails.plugin.springsecurity.userLookup.userDomainClassName", packageName + ".SecurityUser"); + config.put("grails.plugin.springsecurity.userLookup.authorityJoinClassName", packageName + ".SecurityUserRole"); + config.put("grails.plugin.springsecurity.authority.className", packageName + ".SecurityRole"); + config.put("grails.plugin.springsecurity.controllerAnnotations.staticRules", staticRules()); + + generatorContext.addDependency(Dependency.builder() + .groupId("org.apache.grails") + .artifactId("grails-spring-security") + .implementation()); + + generatorContext.addTemplate("springSecurityUserDomain", + new RockerTemplate("grails-app/domain/{packagePath}/SecurityUser.groovy", securityUser.template(project))); + generatorContext.addTemplate("springSecurityRoleDomain", + new RockerTemplate("grails-app/domain/{packagePath}/SecurityRole.groovy", securityRole.template(project))); + generatorContext.addTemplate("springSecurityUserRoleDomain", + new RockerTemplate("grails-app/domain/{packagePath}/SecurityUserRole.groovy", securityUserRole.template(project))); + + // GORM does not autowire services into domain instances by default, so password encoding is + // performed by a persistence-event listener bean — the same approach s2-quickstart generates. + generatorContext.addTemplate("springSecurityPasswordEncoderListener", + new RockerTemplate("src/main/groovy/{packagePath}/SecurityUserPasswordEncoderListener.groovy", + securityUserPasswordEncoderListener.template(project))); + // SpringResources backs off when this feature is selected, so this is the only + // template writing grails-app/conf/spring/resources.groovy + generatorContext.addTemplate("springResources", + new RockerTemplate("grails-app/conf/spring/resources.groovy", springSecurityResources.template(project))); + } + + private static List<Map<String, Object>> staticRules() { + return List.of( + rule("/", "permitAll"), + rule("/error", "permitAll"), + rule("/index", "permitAll"), + rule("/index.gsp", "permitAll"), + rule("/shutdown", "permitAll"), + rule("/assets/**", "permitAll"), + rule("/**/js/**", "permitAll"), + rule("/**/css/**", "permitAll"), + rule("/**/images/**", "permitAll"), + rule("/**/favicon.ico", "permitAll")); + } + + private static Map<String, Object> rule(String pattern, String access) { + return Map.of("pattern", pattern, "access", List.of(access)); + } + + @Override + public boolean supports(ApplicationType applicationType) { + return applicationType == ApplicationType.WEB || applicationType == ApplicationType.REST_API; + } + + @Override + public String getCategory() { + return Category.SECURITY; + } + + @Override + public String getDocumentation() { + return "https://grails.apache.org/docs/" + VersionInfo.getDocumentationVersion() + "/guide/security.html"; + } + +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityRole.rocker.raw b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityRole.rocker.raw new file mode 100644 index 0000000000..f8ae968d11 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityRole.rocker.raw @@ -0,0 +1,50 @@ +@* +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 + +https://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.grails.forge.application.Project + +@args ( +Project project +) + +@if (project.getPackageName() != null) { +package @project.getPackageName() + +} +import grails.compiler.GrailsCompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +@@GrailsCompileStatic +@@EqualsAndHashCode(includes = 'authority') +@@ToString(includes = 'authority', includeNames = true, includePackage = false) +class SecurityRole implements Serializable { + + private static final long serialVersionUID = 1 + + String authority + + static constraints = { + authority nullable: false, blank: false, unique: true + } + + static mapping = { + cache true + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUser.rocker.raw b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUser.rocker.raw new file mode 100644 index 0000000000..13efdf7c63 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUser.rocker.raw @@ -0,0 +1,79 @@ +@* +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 + +https://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.grails.forge.application.Project + +@args ( +Project project +) + +@if (project.getPackageName() != null) { +package @project.getPackageName() + +} +import grails.compiler.GrailsCompileStatic +import grails.plugin.springsecurity.SpringSecurityService +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +@@GrailsCompileStatic +@@EqualsAndHashCode(includes = 'username') +@@ToString(includes = 'username', includeNames = true, includePackage = false) +class SecurityUser implements Serializable { + + private static final long serialVersionUID = 1 + + SpringSecurityService springSecurityService + + String username + String password + boolean enabled = true + boolean accountExpired + boolean accountLocked + boolean passwordExpired + + Set<SecurityRole> getAuthorities() { + (SecurityUserRole.findAllBySecurityUser(this) as List<SecurityUserRole>)*.securityRole as Set<SecurityRole> + } + + def beforeInsert() { + encodePassword() + } + + def beforeUpdate() { + if (isDirty('password')) { + encodePassword() + } + } + + protected void encodePassword() { + password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password + } + + static transients = ['springSecurityService'] + + static constraints = { + password nullable: false, blank: false, password: true + username nullable: false, blank: false, unique: true + } + + static mapping = { + password column: '`password`' + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUserPasswordEncoderListener.rocker.raw b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUserPasswordEncoderListener.rocker.raw new file mode 100644 index 0000000000..aafae39e58 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUserPasswordEncoderListener.rocker.raw @@ -0,0 +1,66 @@ +@* +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 + +https://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.grails.forge.application.Project + +@args ( +Project project +) + +@if (project.getPackageName() != null) { +package @project.getPackageName() + +} +import grails.events.annotation.gorm.Listener +import grails.plugin.springsecurity.SpringSecurityService +import groovy.transform.CompileStatic +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent +import org.grails.datastore.mapping.engine.event.PreInsertEvent +import org.grails.datastore.mapping.engine.event.PreUpdateEvent +import org.springframework.beans.factory.annotation.Autowired + +@@CompileStatic +class SecurityUserPasswordEncoderListener { + + @@Autowired + SpringSecurityService springSecurityService + + @@Listener(SecurityUser) + void onPreInsertEvent(PreInsertEvent event) { + encodePasswordForEvent(event) + } + + @@Listener(SecurityUser) + void onPreUpdateEvent(PreUpdateEvent event) { + encodePasswordForEvent(event) + } + + private void encodePasswordForEvent(AbstractPersistenceEvent event) { + if (event.entityObject instanceof SecurityUser) { + SecurityUser u = event.entityObject as SecurityUser + if (u.password && ((event instanceof PreInsertEvent) || (event instanceof PreUpdateEvent && u.isDirty('password')))) { + event.getEntityAccess().setProperty('password', encodePassword(u.password)) + } + } + } + + private String encodePassword(String password) { + springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUserRole.rocker.raw b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUserRole.rocker.raw new file mode 100644 index 0000000000..c366af2cd2 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/securityUserRole.rocker.raw @@ -0,0 +1,113 @@ +@* +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 + +https://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.grails.forge.application.Project + +@args ( +Project project +) + +@if (project.getPackageName() != null) { +package @project.getPackageName() + +} +import grails.compiler.GrailsCompileStatic +import grails.gorm.DetachedCriteria +import groovy.transform.ToString +import org.codehaus.groovy.util.HashCodeHelper + +@@GrailsCompileStatic +@@ToString(cache = true, includeNames = true, includePackage = false) +class SecurityUserRole implements Serializable { + + private static final long serialVersionUID = 1 + + SecurityUser securityUser + SecurityRole securityRole + + @@Override + boolean equals(other) { + if (other instanceof SecurityUserRole) { + other.securityUserId == securityUser?.id && other.securityRoleId == securityRole?.id + } + } + + @@Override + int hashCode() { + int hashCode = HashCodeHelper.initHash() + if (securityUser) { + hashCode = HashCodeHelper.updateHash(hashCode, securityUser.id) + } + if (securityRole) { + hashCode = HashCodeHelper.updateHash(hashCode, securityRole.id) + } + hashCode + } + + static SecurityUserRole get(long userId, long roleId) { + criteriaFor(userId, roleId).get() + } + + static boolean exists(long userId, long roleId) { + criteriaFor(userId, roleId).count() + } + + private static DetachedCriteria<SecurityUserRole> criteriaFor(long userId, long roleId) { + SecurityUserRole.where { + securityUser == SecurityUser.load(userId) && + securityRole == SecurityRole.load(roleId) + } + } + + static SecurityUserRole create(SecurityUser user, SecurityRole role, boolean flush = false) { + def instance = new SecurityUserRole(securityUser: user, securityRole: role) + instance.save(flush: flush) + instance + } + + static boolean remove(SecurityUser u, SecurityRole r) { + if (u != null && r != null) { + SecurityUserRole.where { securityUser == u && securityRole == r }.deleteAll() + } + } + + static int removeAll(SecurityUser u) { + u == null ? 0 : SecurityUserRole.where { securityUser == u }.deleteAll() as int + } + + static int removeAll(SecurityRole r) { + r == null ? 0 : SecurityUserRole.where { securityRole == r }.deleteAll() as int + } + + static constraints = { + securityUser nullable: false + securityRole nullable: false, validator: { SecurityRole r, SecurityUserRole ur -> + if (ur.securityUser?.id) { + if (SecurityUserRole.exists(ur.securityUser.id, r.id)) { + return ['userRole.exists'] + } + } + } + } + + static mapping = { + id composite: ['securityUser', 'securityRole'] + version false + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/springSecurityResources.rocker.raw b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/springSecurityResources.rocker.raw new file mode 100644 index 0000000000..49a2072220 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/security/template/springSecurityResources.rocker.raw @@ -0,0 +1,33 @@ +@* +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 + +https://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.grails.forge.application.Project + +@args ( +Project project +) + +@if (project.getPackageName() != null) { +import @(project.getPackageName()).SecurityUserPasswordEncoderListener + +} +// Place your Spring DSL code here +beans = { + securityUserPasswordEncoderListener(SecurityUserPasswordEncoderListener) +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java index 79eb828524..fe2f337290 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/spring/SpringResources.java @@ -28,13 +28,17 @@ import org.grails.forge.feature.spring.template.springResources; import org.grails.forge.options.Options; import org.grails.forge.template.RockerTemplate; +import org.grails.forge.feature.security.SpringSecurityCore; + import java.util.Set; @Singleton public class SpringResources implements DefaultFeature { @Override public boolean shouldApply(ApplicationType applicationType, Options options, Set<Feature> selectedFeatures) { - return true; + // The spring-security feature contributes its own resources.groovy registering the + // password-encoder listener bean, so the default empty one must back off + return selectedFeatures.stream().noneMatch(SpringSecurityCore.class::isInstance); } @Override diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/mail/MailSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/mail/MailSpec.groovy new file mode 100644 index 0000000000..63ef25cc8b --- /dev/null +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/mail/MailSpec.groovy @@ -0,0 +1,57 @@ +/* + * 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 + * + * https://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.grails.forge.feature.mail + +import org.grails.forge.ApplicationContextSpec +import org.grails.forge.BuildBuilder +import org.grails.forge.application.generator.GeneratorContext +import org.grails.forge.fixture.CommandOutputFixture + +class MailSpec extends ApplicationContextSpec implements CommandOutputFixture { + + void 'test gradle mail feature adds the plugin dependency'() { + when: + String template = new BuildBuilder(beanContext) + .features(['mail']) + .render() + + then: + template.contains('implementation "org.apache.grails:grails-mail"') + } + + void 'test mail feature configures the smtp defaults'() { + when: + GeneratorContext commandContext = buildGeneratorContext(['mail']) + + then: + commandContext.configuration.get('grails.mail.host') == 'localhost' + commandContext.configuration.get('grails.mail.port') == 25 + } + + void 'test readme.md with feature mail contains links to documentation'() { + when: + def output = generate(['mail']) + def readme = output['README.md'] + + then: + readme + readme.contains('guide/mail.html') + } + +} diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/redis/RedisSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/redis/RedisSpec.groovy new file mode 100644 index 0000000000..859e6fb5d0 --- /dev/null +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/redis/RedisSpec.groovy @@ -0,0 +1,57 @@ +/* + * 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 + * + * https://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.grails.forge.feature.redis + +import org.grails.forge.ApplicationContextSpec +import org.grails.forge.BuildBuilder +import org.grails.forge.application.generator.GeneratorContext +import org.grails.forge.fixture.CommandOutputFixture + +class RedisSpec extends ApplicationContextSpec implements CommandOutputFixture { + + void 'test gradle redis feature adds the plugin dependency'() { + when: + String template = new BuildBuilder(beanContext) + .features(['redis']) + .render() + + then: + template.contains('implementation "org.apache.grails:grails-redis"') + } + + void 'test redis feature configures the connection defaults'() { + when: + GeneratorContext commandContext = buildGeneratorContext(['redis']) + + then: + commandContext.configuration.get('grails.redis.host') == 'localhost' + commandContext.configuration.get('grails.redis.port') == 6379 + } + + void 'test readme.md with feature redis contains links to documentation'() { + when: + def output = generate(['redis']) + def readme = output['README.md'] + + then: + readme + readme.contains('guide/redis.html') + } + +} diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/security/SpringSecurityCoreSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/security/SpringSecurityCoreSpec.groovy new file mode 100644 index 0000000000..4e129938f7 --- /dev/null +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/security/SpringSecurityCoreSpec.groovy @@ -0,0 +1,98 @@ +/* + * 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 + * + * https://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.grails.forge.feature.security + +import org.grails.forge.ApplicationContextSpec +import org.grails.forge.BuildBuilder +import org.grails.forge.application.generator.GeneratorContext +import org.grails.forge.fixture.CommandOutputFixture + +class SpringSecurityCoreSpec extends ApplicationContextSpec implements CommandOutputFixture { + + void 'test gradle spring-security feature adds the plugin dependency'() { + when: + String template = new BuildBuilder(beanContext) + .features(['spring-security']) + .render() + + then: + template.contains('implementation "org.apache.grails:grails-spring-security"') + } + + void 'test spring-security feature configures the domain class lookups'() { + when: + GeneratorContext commandContext = buildGeneratorContext(['spring-security']) + + then: + commandContext.configuration.get('grails.plugin.springsecurity.userLookup.userDomainClassName') == 'example.grails.SecurityUser' + commandContext.configuration.get('grails.plugin.springsecurity.userLookup.authorityJoinClassName') == 'example.grails.SecurityUserRole' + commandContext.configuration.get('grails.plugin.springsecurity.authority.className') == 'example.grails.SecurityRole' + + and: 'the standard permit-all static rules are configured' + List<Map<String, Object>> staticRules = (List<Map<String, Object>>) commandContext.configuration + .get('grails.plugin.springsecurity.controllerAnnotations.staticRules') + staticRules.find { it.pattern == '/' }.access == ['permitAll'] + staticRules.find { it.pattern == '/assets/**' }.access == ['permitAll'] + } + + void 'test spring-security feature generates the security domain classes'() { + when: + def output = generate(['spring-security']) + def user = output['grails-app/domain/example/grails/SecurityUser.groovy'] + def role = output['grails-app/domain/example/grails/SecurityRole.groovy'] + def userRole = output['grails-app/domain/example/grails/SecurityUserRole.groovy'] + + then: + user + user.contains('package example.grails') + user.contains('class SecurityUser implements Serializable') + user.contains('Set<SecurityRole> getAuthorities()') + user.contains('protected void encodePassword()') + + and: + role + role.contains('class SecurityRole implements Serializable') + role.contains('String authority') + + and: + userRole + userRole.contains('class SecurityUserRole implements Serializable') + userRole.contains("id composite: ['securityUser', 'securityRole']") + + and: 'the password encoder listener is generated and registered as a bean' + def listener = output['src/main/groovy/example/grails/SecurityUserPasswordEncoderListener.groovy'] + listener + listener.contains('class SecurityUserPasswordEncoderListener') + listener.contains('@Listener(SecurityUser)') + def resources = output['grails-app/conf/spring/resources.groovy'] + resources.contains('import example.grails.SecurityUserPasswordEncoderListener') + resources.contains('securityUserPasswordEncoderListener(SecurityUserPasswordEncoderListener)') + } + + void 'test readme.md with feature spring-security contains links to documentation'() { + when: + def output = generate(['spring-security']) + def readme = output['README.md'] + + then: + readme + readme.contains('guide/security.html') + } + +}
