diff --git a/plugin/grails-app/commands/grails.plugin.springsecurity/CommandLineHelper.groovy b/plugin/grails-app/commands/grails.plugin.springsecurity/CommandLineHelper.groovy new file mode 100644 index 000000000..0133412d9 --- /dev/null +++ b/plugin/grails-app/commands/grails.plugin.springsecurity/CommandLineHelper.groovy @@ -0,0 +1,34 @@ +package grails.plugin.springsecurity + +import grails.dev.commands.ExecutionContext +import io.micronaut.core.naming.NameUtils +import org.grails.build.parsing.CommandLine + +trait CommandLineHelper { + + static final boolean SUCCESS = true + static final boolean FAILURE = false + + abstract ExecutionContext getExecutionContext() + + boolean isFlagPresent(String name) { + final CommandLine commandLine = executionContext.commandLine + if (commandLine.hasOption(name)) { + return commandLine.optionValue(name) ? true : false + } else { + def value = commandLine?.undeclaredOptions?.get(name) + return value ? true : false + } + } + + String flagValue(String name) { + final CommandLine commandLine = executionContext.commandLine + if (commandLine.hasOption(name)) { + return commandLine.optionValue(name) + } else { + def value = commandLine?.undeclaredOptions?.get(name) + return value + } + } + +} \ No newline at end of file diff --git a/plugin/grails-app/commands/grails.plugin.springsecurity/S2CreatePersistentTokenCommand.groovy b/plugin/grails-app/commands/grails.plugin.springsecurity/S2CreatePersistentTokenCommand.groovy new file mode 100644 index 000000000..fc60ebe4e --- /dev/null +++ b/plugin/grails-app/commands/grails.plugin.springsecurity/S2CreatePersistentTokenCommand.groovy @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Puneet Behl. + * + * 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. + */ +package grails.plugin.springsecurity + +import grails.build.logging.ConsoleLogger +import grails.build.logging.GrailsConsole +import grails.codegen.model.Model +import grails.dev.commands.GrailsApplicationCommand +import groovy.transform.CompileStatic + +/** + * Creates a persistent token domain class for the Spring Security Core plugin. + * Usage: ./gradlew runCommand "-Pargs=s2-create-persistent-token [DOMAIN CLASS NAME]" +* + * For example: ./gradlew runCommand "-Pargs=s2-create-persistent-token com.yourapp.PersistentLogin" + * + * @author Puneet Behl + * @since 6.0.0 + */ +@CompileStatic +class S2CreatePersistentTokenCommand implements GrailsApplicationCommand, CommandLineHelper, SkipBootstrap { + + @Delegate + ConsoleLogger consoleLogger = GrailsConsole.getInstance() + + private final static USAGE_MESSAGE = ''' +./gradlew runCommand "-Pargs=s2-create-persistent-token [DOMAIN CLASS NAME]" + +For example: ./gradlew runCommand "-Pargs=s2-create-persistent-token com.yourapp.PersistentLogin" +''' + + @Override + boolean handle() { + + if (args.size() == 0) { + consoleLogger.error("Usage: " + USAGE_MESSAGE) + return FAILURE + } + + final String domainClass = args[0] + final Model domainModel = model(domainClass) + consoleLogger.addStatus ("\nCreating persistent token class $domainClass") + render(template: template("PersistentLogin.groovy.template"), + destination: file("grails-app/domain/$domainModel.packagePath/${domainModel.simpleName}.groovy"), + model: domainModel, + overrite: false + ) + file('grails-app/conf/application.groovy').withWriterAppend { BufferedWriter writer -> + writer.newLine() + writer.writeLine 'grails.plugin.springsecurity.rememberMe.persistent = true' + writer.writeLine "grails.plugin.springsecurity.rememberMe.persistentToken.domainClassName = '$domainClass'" + writer.newLine() + } + return SUCCESS + } +} diff --git a/plugin/grails-app/commands/grails.plugin.springsecurity/S2CreateRoleHierarchyEntryCommand.groovy b/plugin/grails-app/commands/grails.plugin.springsecurity/S2CreateRoleHierarchyEntryCommand.groovy new file mode 100644 index 000000000..88f0a9c5e --- /dev/null +++ b/plugin/grails-app/commands/grails.plugin.springsecurity/S2CreateRoleHierarchyEntryCommand.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Puneet Behl. + * + * 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. + */ +package grails.plugin.springsecurity + +import grails.build.logging.ConsoleLogger +import grails.build.logging.GrailsConsole +import grails.codegen.model.Model +import grails.dev.commands.GrailsApplicationCommand +import groovy.transform.CompileStatic + +/** + * Creates a domain class for a persistent role hierarchy for the Spring Security Core plugin + * Usage: ./gradlew runCommand "-Pargs=s2-create-role-hierarchy-entry [DOMAIN CLASS NAME]" + * For example: ./gradlew runCommand "-Pargs=s2-create-role-hierarchy-entry com.yourapp.RoleHierarchyEntry" + * + * @author Puneet Behl + * @since 6.0.0 + */ +@CompileStatic +class S2CreateRoleHierarchyEntryCommand implements GrailsApplicationCommand, CommandLineHelper, SkipBootstrap { + + private static final String USAGE_MESSAGE = ''' +./gradlew runCommand "-Pargs=s2-create-role-hierarchy-entry [DOMAIN CLASS NAME]" + + For example: ./gradlew runCommand "-Pargs=s2-create-role-hierarchy-entry com.yourapp.RoleHierarchyEntry" +''' + + @Delegate + ConsoleLogger consoleLogger = GrailsConsole.getInstance() + + @Override + boolean handle() { + + if (args.size() == 0) { + consoleLogger.error("Usage: " + USAGE_MESSAGE) + return FAILURE + } + + final String domainClass = args[0] + final Model domainModel = model(domainClass) + + consoleLogger.addStatus("\nCreating role hierarchy entry class $domainClass") + render(template: template('RoleHierarchyEntry.groovy.template'), + destination: file("grails-app/domain/$domainModel.packagePath/${domainModel.simpleName}.groovy"), + model: domainModel, overwrite: false) + + file('grails-app/conf/application.groovy').withWriterAppend { BufferedWriter writer -> + writer.newLine() + writer.writeLine "grails.plugin.springsecurity.roleHierarchyEntryClassName = '$domainClass'" + writer.newLine() + } + + return SUCCESS + } +} diff --git a/plugin/grails-app/commands/grails.plugin.springsecurity/S2QuickstartCommand.groovy b/plugin/grails-app/commands/grails.plugin.springsecurity/S2QuickstartCommand.groovy new file mode 100644 index 000000000..ed331615c --- /dev/null +++ b/plugin/grails-app/commands/grails.plugin.springsecurity/S2QuickstartCommand.groovy @@ -0,0 +1,316 @@ +/* + * Copyright 2023 Puneet Behl. + * + * 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. + */ +package grails.plugin.springsecurity + +import grails.build.logging.ConsoleLogger +import grails.build.logging.GrailsConsole +import grails.codegen.model.Model +import grails.dev.commands.GrailsApplicationCommand +import groovy.transform.CompileStatic + +/** + * Creates domain classes and updates config settings for the Spring Security plugin. + * Usage: ./gradlew runCommand "-Pargs=s2-quickstart [DOMAIN_CLASS_PACKAGE] [USER_CLASS_NAME] [ROLE_CLASS_NAME] [REQUEST_MAP_CLASS_NAME] --groupClassName=[GROUP_CLASS_NAME]" or + * s2-quickstart --ui-only + * + * For Example: + * 1. ./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role --groupClassName=RoleGroup" + * 2. ./gradlew runCommand "-Pargs=s2-quickstart com.yourapp Person Authority Requestmap" + * 3. ./gradlew runCommand "-Pargs=s2-quickstart --uiOnly" + * 4. ./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role" + * + * @author Puneet Behl + * @since 6.0.0 + */ +@CompileStatic +class S2QuickstartCommand implements GrailsApplicationCommand, CommandLineHelper, SkipBootstrap { + + public static final String GORM_VERSION_THRESHOLD = '6.0.10' + private Map templateAttributes + private boolean uiOnly + private boolean salt + private String packageName + private Model userModel + private Model roleModel + private Model requestmapModel + private Model roleGroupModel + + String description = 'Creates domain classes and updates config settings for the Spring Security plugin.' + + private final static String USAGE_MESSAGE = ''' + ./gradlew runCommand "-Pargs=s2-quickstart [DOMAIN-CLASS-PACKAGE] [USER-CLASS-NAME] [ROLE-CLASS-NAME] [REQUESTMAP-CLASS-NAME] --groupClassName=GROUP-CLASS-NAME" +or ./gradlew runCommand "-Pargs=s2-quickstart --uiOnly" + +Example: ./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role" +Example: ./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role --groupClassName=RoleGroup" +Example: ./gradlew runCommand "-Pargs=s2-quickstart com.yourapp Person Authority Requestmap" +Example: ./gradlew runCommand "-Pargs=s2-quickstart --uiOnly" +''' + + @Delegate + ConsoleLogger consoleLogger = GrailsConsole.getInstance() + + @Override + boolean handle() { + + if (uiOnly) { + consoleLogger.addStatus('\nConfiguring Spring Security; not generating domain classes') + } else { + if (args.size() < 3) { + error('Usage:' + USAGE_MESSAGE) + return FAILURE + } + initialize() + initializeTemplateAttributes() + createDomains(userModel, roleModel, requestmapModel, roleGroupModel) + } + + updateConfig(userModel?.simpleName, roleModel?.simpleName, requestmapModel?.simpleName, userModel?.packageName, roleGroupModel != null) + logStatus() + return SUCCESS + } + + private void logStatus() { + if (uiOnly) { + consoleLogger.addStatus ''' +************************************************************ +* Your grails-app/conf/application.groovy has been updated * +* with security settings; please verify that the * +* values are correct. * +************************************************************ +''' + } else { + consoleLogger.addStatus ''' +************************************************************ +* Created security-related domain classes. Your * +* grails-app/conf/application.groovy has been updated with * +* the class names of the configured domain classes; * +* please verify that the values are correct. * +************************************************************ +''' + } + } + + private void initializeTemplateAttributes() { + templateAttributes = Collections.unmodifiableMap([ + packageName : userModel.packageName, + userClassName : userModel.simpleName, + userClassProperty : userModel.modelName, + roleClassName : roleModel.simpleName, + roleClassProperty : roleModel.modelName, + requestmapClassName: requestmapModel?.simpleName, + groupClassName : roleGroupModel?.simpleName, + groupClassProperty : roleGroupModel?.modelName]) + } + + private void initialize() { + uiOnly = isFlagPresent('uiOnly') + salt = flagValue('salt') + + packageName = args[0] + userModel = model(packageName + '.' + args[1]) + if (userModel) { + consoleLogger.addStatus('\nCreating User class ' + userModel.simpleName + ' in package ' + packageName) + } + roleModel = model(packageName + '.' + args[2]) + if (roleModel) { + consoleLogger.addStatus('\nCreating Role class ' + roleModel.simpleName + ' in package ' + packageName) + } + final String groupClassName = flagValue('groupClassName') + roleGroupModel = groupClassName ? model(packageName + '.' + groupClassName) : null + if (roleGroupModel) { + consoleLogger.addStatus('\nCreating Role/Group classes ' + roleGroupModel.simpleName + ' in package ' + packageName) + } + } + + private Map extractVersion(String versionString) { + String[] arr = versionString.split('\\.') + Map v = new HashMap<>([mayor: 0, minor: 0, bug: 0]) + try { + if (arr.size() >= 1) { + v.mayor = arr[0].toInteger() + } + if (arr.size() >= 2) { + v.minor = arr[1].toInteger() + } + if (arr.size() >= 3) { + v.bug = arr[2].toInteger() + } + } catch (Exception e) { + v = [mayor: 0, minor: 0, bug: 0] + } + v + } + + private boolean versionAfterOrEqualsToThreshold(String threshold, String value) { + if (value == null) { + return false + } + if (value.startsWith(threshold)) { + return true + } + + Map va = extractVersion(value) + Map vb = extractVersion(threshold) + List> l = [va, vb] + l.sort { a, b -> + def compare = a.mayor <=> b.mayor + if (compare != 0) { + return compare + } + compare = a.minor <=> b.minor + if (compare != 0) { + return compare + } + a.bug <=> b.bug + } + String sortedValue = l[0].collect { k, v -> v }.join('.') + threshold.startsWith(sortedValue) + } + + private void createDomains(Model userModel, + Model roleModel, + Model requestmapModel, + Model groupModel) { + + final Properties props = new Properties() + file("gradle.properties")?.withInputStream { props.load(it) } + + boolean gormVersionAfterThreshold = versionAfterOrEqualsToThreshold(GORM_VERSION_THRESHOLD, + (String) props.gormVersion ?: (String) props.getProperty("gorm.version")) + + if (gormVersionAfterThreshold) { + generateFile('PersonWithoutInjection', userModel.packagePath, userModel.simpleName) + if (salt) { + generateFile('PersonPasswordEncoderListenerWithSalt', + userModel.packagePath, + userModel.simpleName, + "${userModel.simpleName}PasswordEncoderListener", 'src/main/groovy') + } else { + generateFile('PersonPasswordEncoderListener', + userModel.packagePath, + userModel.simpleName, + "${userModel.simpleName}PasswordEncoderListener", + 'src/main/groovy') + } + List> beans = [] + beans.add([import : "import ${userModel.packageName}.${userModel.simpleName}PasswordEncoderListener".toString(), + definition: "${userModel.propertyName}PasswordEncoderListener(${userModel.simpleName}PasswordEncoderListener)".toString()]) + addBeans(beans, 'grails-app/conf/spring/resources.groovy') + + } else { + if (salt) { + generateFile('PersonWithSalt', userModel.packagePath, userModel.simpleName) + } else { + generateFile('Person', userModel.packagePath, userModel.simpleName) + } + } + + generateFile('Authority', roleModel.packagePath, roleModel.simpleName) + generateFile('PersonAuthority', roleModel.packagePath, userModel.simpleName + roleModel.simpleName) + + if (requestmapModel) { + generateFile('Requestmap', requestmapModel.packagePath, requestmapModel.simpleName) + } + + if (groupModel) { + generateFile('AuthorityGroup', groupModel.packagePath, groupModel.simpleName) + generateFile('PersonAuthorityGroup', groupModel.packagePath, userModel.simpleName + groupModel.simpleName) + generateFile('AuthorityGroupAuthority', groupModel.packagePath, groupModel.simpleName + roleModel.simpleName) + } + } + + private void updateConfig(String userClassName, String roleClassName, String requestmapClassName, String packageName, boolean useRoleGroups) { + + file('grails-app/conf/application.groovy').withWriterAppend { BufferedWriter writer -> + writer.newLine() + writer.newLine() + writer.writeLine('// Added by the Spring Security Core plugin:') + if (!uiOnly) { + writer.writeLine("grails.plugin.springsecurity.userLookup.userDomainClassName = '${packageName}.$userClassName'") + writer.writeLine("grails.plugin.springsecurity.userLookup.authorityJoinClassName = '${packageName}.$userClassName$roleClassName'") + writer.writeLine("grails.plugin.springsecurity.authority.className = '${packageName}.$roleClassName'") + } + if (useRoleGroups) { + writer.writeLine("grails.plugin.springsecurity.authority.groupAuthorityNameField = 'authorities'") + writer.writeLine('grails.plugin.springsecurity.useRoleGroups = true') + } + if (requestmapClassName) { + writer.writeLine("grails.plugin.springsecurity.requestMap.className = '${packageName}.$requestmapClassName'") + writer.writeLine("grails.plugin.springsecurity.securityConfigType = 'Requestmap'") + } + writer.writeLine('grails.plugin.springsecurity.controllerAnnotations.staticRules = [') + writer.writeLine("\t[pattern: '/', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/error', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/index', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/index.gsp', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/shutdown', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/assets/**', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/**/js/**', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/**/css/**', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/**/images/**', access: ['permitAll']],") + writer.writeLine("\t[pattern: '/**/favicon.ico', access: ['permitAll']]") + writer.writeLine(']') + writer.newLine() + + writer.writeLine('grails.plugin.springsecurity.filterChain.chainMap = [') + writer.writeLine("\t[pattern: '/assets/**', filters: 'none'],") + writer.writeLine("\t[pattern: '/**/js/**', filters: 'none'],") + writer.writeLine("\t[pattern: '/**/css/**', filters: 'none'],") + writer.writeLine("\t[pattern: '/**/images/**', filters: 'none'],") + writer.writeLine("\t[pattern: '/**/favicon.ico', filters: 'none'],") + writer.writeLine("\t[pattern: '/**', filters: 'JOINED_FILTERS']") + writer.writeLine(']') + writer.newLine() + } + } + + private void generateFile(String templateName, String packagePath, String className, String fileName = null, String folder = 'grails-app/domain') { + render template(templateName + '.groovy.template'), + file("${folder}/$packagePath/${fileName ?: className}.groovy"), + templateAttributes, false + } + + private void addBeans(List> beans, String resourceConfigFilePath) { + final File resourceConfig = new File(resourceConfigFilePath) + List lines = [] + beans.forEach(bean -> lines.add(bean.import)) + if (resourceConfig.exists()) { + resourceConfig.eachLine { line, nb -> + lines << line + if (line.contains('beans = {')) { + beans.each { Map bean -> + lines << ' ' + bean.definition + } + } + } + } else { + lines << 'beans = {' + beans.each { Map bean -> + lines << ' ' + bean.definition + } + lines << '}' + } + + resourceConfig.withWriter('UTF-8') { writer -> + lines.each { String line -> + writer.write "${line}${System.lineSeparator()}" + } + } + } + +} + diff --git a/plugin/grails-app/commands/grails.plugin.springsecurity/SkipBootstrap.groovy b/plugin/grails-app/commands/grails.plugin.springsecurity/SkipBootstrap.groovy new file mode 100644 index 000000000..2e3d3c78b --- /dev/null +++ b/plugin/grails-app/commands/grails.plugin.springsecurity/SkipBootstrap.groovy @@ -0,0 +1,7 @@ +package grails.plugin.springsecurity + +trait SkipBootstrap { + + boolean skipBootstrap = true + +} \ No newline at end of file diff --git a/plugin/src/main/scripts/s2-create-persistent-token.groovy b/plugin/src/main/scripts/s2-create-persistent-token.groovy deleted file mode 100644 index 3ff7fdc2b..000000000 --- a/plugin/src/main/scripts/s2-create-persistent-token.groovy +++ /dev/null @@ -1,42 +0,0 @@ -/* Copyright 2006-2016 the original author or authors. - * - * 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 grails.codegen.model.Model - -description 'Creates a persistent token domain class for the Spring Security Core plugin', { - usage ''' -grails s2-create-persistent-token [DOMAIN CLASS NAME] - -Example: grails s2-create-persistent-token com.yourapp.PersistentLogin -''' - - argument name: 'Domain class name', description: 'The domain class full name with package' -} - -String fullClassName = args[0] -Model model = model(fullClassName) - -addStatus "\nCreating persistent token class $fullClassName" - -render template: template('PersistentLogin.groovy.template'), - destination: file("grails-app/domain/$model.packagePath/${model.simpleName}.groovy"), - model: model, overwrite: false - -file('grails-app/conf/application.groovy').withWriterAppend { BufferedWriter writer -> - writer.newLine() - writer.writeLine 'grails.plugin.springsecurity.rememberMe.persistent = true' - writer.writeLine "grails.plugin.springsecurity.rememberMe.persistentToken.domainClassName = '$fullClassName'" - writer.newLine() -} diff --git a/plugin/src/main/scripts/s2-create-role-hierarchy-entry.groovy b/plugin/src/main/scripts/s2-create-role-hierarchy-entry.groovy deleted file mode 100644 index 669f17b3f..000000000 --- a/plugin/src/main/scripts/s2-create-role-hierarchy-entry.groovy +++ /dev/null @@ -1,46 +0,0 @@ -/* Copyright 2015-2016 the original author or authors. - * - * 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 grails.codegen.model.Model - -/** - * @author fpape - * @author Burt Beckwith - */ - -description 'Creates a domain class for a persistent role hierarchy for the Spring Security Core plugin', { - usage ''' -grails s2-create-role-hierarchy-entry [DOMAIN CLASS NAME] - -Example: grails s2-create-role-hierarchy-entry com.yourapp.RoleHierarchyEntry -''' - - argument name: 'Domain class name', description: 'The domain class full name with package' -} - -String fullClassName = args[0] -Model model = model(fullClassName) - -addStatus "\nCreating role hierarchy entry class $fullClassName" - -render template: template('RoleHierarchyEntry.groovy.template'), - destination: file("grails-app/domain/$model.packagePath/${model.simpleName}.groovy"), - model: model, overwrite: false - -file('grails-app/conf/application.groovy').withWriterAppend { BufferedWriter writer -> - writer.newLine() - writer.writeLine "grails.plugin.springsecurity.roleHierarchyEntryClassName = '$fullClassName'" - writer.newLine() -} diff --git a/plugin/src/main/scripts/s2-quickstart.groovy b/plugin/src/main/scripts/s2-quickstart.groovy deleted file mode 100644 index 3d02fadc0..000000000 --- a/plugin/src/main/scripts/s2-quickstart.groovy +++ /dev/null @@ -1,284 +0,0 @@ -/* Copyright 2006-2016 the original author or authors. - * - * 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 grails.codegen.model.Model -import groovy.transform.Field - -@Field String usageMessage = ''' - grails s2-quickstart [requestmap-class-name] [--groupClassName=group-class-name] -or grails s2-quickstart --uiOnly - -Example: grails s2-quickstart com.yourapp User Role -Example: grails s2-quickstart com.yourapp User Role --groupClassName=RoleGroup -Example: grails s2-quickstart com.yourapp Person Authority Requestmap -Example: grails s2-quickstart --uiOnly -''' - -@Field Map templateAttributes -@Field boolean uiOnly -@Field boolean salt - -description 'Creates domain classes and updates config settings for the Spring Security plugin', { - - usage usageMessage - - argument name: 'Domain class package', description: 'The package to use for the domain classes', required: false - argument name: 'User class name', description: 'The name of the User/Person class', required: false - argument name: 'Role class name', description: 'The name of the Role class', required: false - argument name: 'Requestmap class name', description: 'The name of the Requestmap class', required: false - - flag name: 'groupClassName', description: 'If specified, role/group classes will also be generated using the flag value as the role-group name' - flag name: 'uiOnly', description: 'If specified, no domain classes are created but the plugin settings are initialized (useful with LDAP, Mock, Shibboleth, etc.)' -} - -Model userModel -Model roleModel -Model requestmapModel -Model groupModel -uiOnly = flag('uiOnly') -salt = flag('salt') -if (uiOnly) { - addStatus '\nConfiguring Spring Security; not generating domain classes' -} -else { - - if (args.size() < 3) { - error 'Usage:' + usageMessage - return false - } - - String packageName = args[0] - String groupClassName = flag('groupClassName') - String groupClassNameMessage = '' - if (groupClassName) { - groupModel = model(packageName + '.' + groupClassName) - groupClassNameMessage = ", and role/group classes for '" + groupModel.simpleName + "'" - } - - userModel = model(packageName + '.' + args[1]) - roleModel = model(packageName + '.' + args[2]) - - String message = "Creating User class '" + userModel.simpleName + "'" - if (4 == args.size()) { - requestmapModel = model(packageName + '.' + args[3]) - message += ", Role class '" + roleModel.simpleName + "', and Requestmap class '" + requestmapModel.simpleName + "'" + groupClassNameMessage - } - else { - message += " and Role class '" + roleModel.simpleName + "'" + groupClassNameMessage - } - message += " in package '" + packageName + "'" - addStatus message - - templateAttributes = [ - packageName: userModel.packageName, - userClassName: userModel.simpleName, - userClassProperty: userModel.modelName, - roleClassName: roleModel.simpleName, - roleClassProperty: roleModel.modelName, - requestmapClassName: requestmapModel?.simpleName, - groupClassName: groupModel?.simpleName, - groupClassProperty: groupModel?.modelName] - - createDomains userModel, roleModel, requestmapModel, groupModel -} - -updateConfig userModel?.simpleName, roleModel?.simpleName, requestmapModel?.simpleName, userModel?.packageName, groupModel != null - -if (uiOnly) { - addStatus ''' -************************************************************ -* Your grails-app/conf/application.groovy has been updated * -* with security settings; please verify that the * -* values are correct. * -************************************************************ -''' -} -else { - addStatus ''' -************************************************************ -* Created security-related domain classes. Your * -* grails-app/conf/application.groovy has been updated with * -* the class names of the configured domain classes; * -* please verify that the values are correct. * -************************************************************ -''' -} - -private Map extractVersion(String versionString) { - def arr = versionString.split('\\.') - def v = [mayor: 0, minor: 0, bug: 0] - try { - if ( arr.size() >= 1) { - v.mayor = arr[0].toInteger() - } - if ( arr.size() >= 2) { - v.minor = arr[1].toInteger() - } - if ( arr.size() >= 3) { - v.bug = arr[2].toInteger() - } - } catch ( Exception e ) { - v = [mayor: 0, minor: 0, bug: 0] - } - v -} - -private boolean versionAfterOrEqualsToThreshold(String threshold, String value) { - if ( value == null ) { - return false - } - if ( value.startsWith(threshold) ) { - return true - } - - def va = extractVersion(value) - def vb = extractVersion(threshold) - def l = [va, vb] - l.sort { Map a, Map b -> - def compare = a.mayor <=> b.mayor - if ( compare != 0 ) { - return compare - } - compare = a.minor <=> b.minor - if ( compare != 0 ) { - return compare - } - a.bug <=> b.bug - } - def sortedValue = l[0].collect { k, v -> v }.join('.') - threshold.startsWith(sortedValue) -} - -private void createDomains(Model userModel, Model roleModel, Model requestmapModel, Model groupModel) { - - def props = new Properties() - file("gradle.properties")?.withInputStream { props.load(it) } - - final threshold = '6.0.10' - - boolean gormVersionAfterThreshold = versionAfterOrEqualsToThreshold(threshold, props.gormVersion ?: props.getProperty("gorm.version")) - - if ( gormVersionAfterThreshold ) { - generateFile 'PersonWithoutInjection', userModel.packagePath, userModel.simpleName - if ( salt ) { - generateFile 'PersonPasswordEncoderListenerWithSalt', userModel.packagePath, userModel.simpleName, "${userModel.simpleName}PasswordEncoderListener", 'src/main/groovy' - } else { - generateFile 'PersonPasswordEncoderListener', userModel.packagePath, userModel.simpleName, "${userModel.simpleName}PasswordEncoderListener", 'src/main/groovy' - } - def beansList = [[import: "import ${userModel.packageName}.${userModel.simpleName}PasswordEncoderListener", definition: "${userModel.propertyName}PasswordEncoderListener(${userModel.simpleName}PasswordEncoderListener)"]] - addBeans(beansList, 'grails-app/conf/spring/resources.groovy') - - } else { - if ( salt ) { - generateFile 'PersonWithSalt', userModel.packagePath, userModel.simpleName - } else { - generateFile 'Person', userModel.packagePath, userModel.simpleName - } - } - - generateFile 'Authority', roleModel.packagePath, roleModel.simpleName - generateFile 'PersonAuthority', roleModel.packagePath, userModel.simpleName + roleModel.simpleName - - if (requestmapModel) { - generateFile 'Requestmap', requestmapModel.packagePath, requestmapModel.simpleName - } - - if (groupModel) { - generateFile 'AuthorityGroup', groupModel.packagePath, groupModel.simpleName - generateFile 'PersonAuthorityGroup', groupModel.packagePath, userModel.simpleName + groupModel.simpleName - generateFile 'AuthorityGroupAuthority', groupModel.packagePath, groupModel.simpleName + roleModel.simpleName - } -} - -private void updateConfig(String userClassName, String roleClassName, String requestmapClassName, String packageName, boolean useRoleGroups) { - - file('grails-app/conf/application.groovy').withWriterAppend { BufferedWriter writer -> - writer.newLine() - writer.newLine() - writer.writeLine '// Added by the Spring Security Core plugin:' - if (!uiOnly) { - writer.writeLine "grails.plugin.springsecurity.userLookup.userDomainClassName = '${packageName}.$userClassName'" - writer.writeLine "grails.plugin.springsecurity.userLookup.authorityJoinClassName = '${packageName}.$userClassName$roleClassName'" - writer.writeLine "grails.plugin.springsecurity.authority.className = '${packageName}.$roleClassName'" - } - if (useRoleGroups) { - writer.writeLine "grails.plugin.springsecurity.authority.groupAuthorityNameField = 'authorities'" - writer.writeLine 'grails.plugin.springsecurity.useRoleGroups = true' - } - if (requestmapClassName) { - writer.writeLine "grails.plugin.springsecurity.requestMap.className = '${packageName}.$requestmapClassName'" - writer.writeLine "grails.plugin.springsecurity.securityConfigType = 'Requestmap'" - } - writer.writeLine 'grails.plugin.springsecurity.controllerAnnotations.staticRules = [' - writer.writeLine "\t[pattern: '/', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/error', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/index', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/index.gsp', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/shutdown', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/assets/**', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/**/js/**', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/**/css/**', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/**/images/**', access: ['permitAll']]," - writer.writeLine "\t[pattern: '/**/favicon.ico', access: ['permitAll']]" - writer.writeLine ']' - writer.newLine() - - writer.writeLine 'grails.plugin.springsecurity.filterChain.chainMap = [' - writer.writeLine "\t[pattern: '/assets/**', filters: 'none']," - writer.writeLine "\t[pattern: '/**/js/**', filters: 'none']," - writer.writeLine "\t[pattern: '/**/css/**', filters: 'none']," - writer.writeLine "\t[pattern: '/**/images/**', filters: 'none']," - writer.writeLine "\t[pattern: '/**/favicon.ico', filters: 'none']," - writer.writeLine "\t[pattern: '/**', filters: 'JOINED_FILTERS']" - writer.writeLine ']' - writer.newLine() - } -} - -private void generateFile(String templateName, String packagePath, String className, String fileName = null, String folder = 'grails-app/domain') { - render template(templateName + '.groovy.template'), - file("${folder}/$packagePath/${fileName ?: className}.groovy"), - templateAttributes, false -} - -private void addBeans(List beans, String pathname) { - def f = new File(pathname) - def lines = [] - beans.each { Map bean -> - lines << bean.import - } - if ( f.exists() ) { - f.eachLine { line, nb -> - lines << line - if ( line.contains('beans = {') ) { - beans.each { Map bean -> - lines << ' ' + bean.definition - } - } - } - } else { - lines << 'beans = {' - beans.each { Map bean -> - lines << ' ' + bean.definition - } - lines << '}' - } - - f.withWriter('UTF-8') { writer -> - lines.each { String line -> - writer.write "${line}${System.lineSeparator()}" - } - } -}