| /* |
| * 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 |
| * |
| * 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 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: <code>./grailsw s2-quickstart [DOMAIN_CLASS_PACKAGE] [USER_CLASS_NAME] [ROLE_CLASS_NAME] [REQUEST_MAP_CLASS_NAME] --groupClassName=[GROUP_CLASS_NAME]</code> or |
| * <code>s2-quickstart --ui-only</code> |
| * |
| * For Example: |
| * 1. <code>./grailsw s2-quickstart com.yourapp User Role --groupClassName=RoleGroup</code> |
| * 2. <code>./grailsw s2-quickstart com.yourapp Person Authority Requestmap</code> |
| * 3. <code>./grailsw s2-quickstart --uiOnly</code> |
| * 4. <code>./grailsw s2-quickstart s2-quickstart com.yourapp User Role</code> |
| * |
| * @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<String, String> 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 = ''' |
| ./grailsw s2-quickstart [DOMAIN-CLASS-PACKAGE] [USER-CLASS-NAME] [ROLE-CLASS-NAME] [REQUESTMAP-CLASS-NAME] --groupClassName=GROUP-CLASS-NAME |
| or ./grailsw s2-quickstart --uiOnly |
| |
| Example: ./grailsw s2-quickstart s2-quickstart com.yourapp User Role |
| Example: ./grailsw s2-quickstart com.yourapp User Role --groupClassName=RoleGroup |
| Example: ./grailsw s2-quickstart com.yourapp Person Authority Requestmap |
| Example: ./grailsw 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) |
| } |
| if (new File('grails-app/conf/application.yml').exists()) { |
| consoleLogger.addStatus('\nConfiguring Spring Security; Updating YAML Application Config') |
| updateYmlConfig(userModel?.simpleName, roleModel?.simpleName, requestmapModel?.simpleName, userModel?.packageName, roleGroupModel != null) |
| } else { |
| 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<String, Integer> extractVersion(String versionString) { |
| String[] arr = versionString.split('\\.') |
| Map<String, Integer> 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<String, Integer> va = extractVersion(value) |
| Map<String, Integer> vb = extractVersion(threshold) |
| List<Map<String, Integer>> 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) } |
| |
| 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<Map<String, String>> beans = [] |
| beans.add([import : "import ${userModel.packageName}.${userModel.simpleName}PasswordEncoderListener".toString(), |
| definition: "${userModel.propertyName}PasswordEncoderListener(${userModel.simpleName}PasswordEncoderListener)".toString()]) |
| |
| if(new File('grails-app/conf/spring/resources.groovy').exists()) { |
| addBeans(beans, 'grails-app/conf/spring/resources.groovy') |
| } else { |
| //we could be generating this in a plugin... we should look for a Plugin class |
| File pluginClassFile = findPluginClass() |
| if(pluginClassFile != null) { |
| addBeansToPlugin(beans, pluginClassFile) |
| } |
| } |
| |
| |
| 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 updateYmlConfig(String userClassName, String roleClassName, String requestmapClassName, String packageName, boolean useRoleGroups) { |
| //I mean, we could use SNAKE YAML TO GENERATE THIS BUT I DIGRESS |
| file('grails-app/conf/application.yml').withWriterAppend { BufferedWriter writer -> |
| writer.newLine() |
| writer.writeLine('---') |
| writer.writeLine('# Added by the Spring Security Core plugin:') |
| writer.writeLine('grails:') |
| writer.writeLine(' plugin:') |
| writer.writeLine(' springsecurity:') |
| if (!uiOnly) { |
| writer.writeLine(' userLookup:') |
| writer.writeLine(" userDomainClassName: '${packageName}.$userClassName'") |
| writer.writeLine(" authorityJoinClassName: '${packageName}.$userClassName$roleClassName'") |
| } |
| if(!uiOnly || useRoleGroups) { |
| writer.writeLine(' authority:') |
| if(!uiOnly) { |
| writer.writeLine(" className: '${packageName}.$roleClassName'") |
| } |
| if(useRoleGroups) { |
| writer.writeLine(" groupAuthorityNameField: authorities") |
| } |
| } |
| |
| if (useRoleGroups) { |
| writer.writeLine(' useRoleGroups: true') |
| } |
| if (requestmapClassName) { |
| writer.writeLine(" requestMap.className: '${packageName}.$requestmapClassName'") |
| writer.writeLine(" securityConfigType: Requestmap") |
| } |
| writer.writeLine(' controllerAnnotations:') |
| writer.writeLine(' staticRules:') |
| writer.writeLine(' - pattern: /') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /error') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /index') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /index.gsp') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /shutdown') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /assets/**') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /**/js/**') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /**/css/**') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /**/images/**') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.writeLine(' - pattern: /**/favicon.ico') |
| writer.writeLine(' access: [\'permitAll\']') |
| writer.newLine() |
| |
| writer.writeLine(' filterChain:') |
| writer.writeLine(' chainMap:') |
| writer.writeLine(' - pattern: /assets/**') |
| writer.writeLine(' filters: none') |
| writer.writeLine(' - pattern: /**/js/**') |
| writer.writeLine(' filters: none') |
| writer.writeLine(' - pattern: /**/css/**') |
| writer.writeLine(' filters: none') |
| writer.writeLine(' - pattern: /**/images/**') |
| writer.writeLine(' filters: none') |
| writer.writeLine(' - pattern: /**/favicon.ico') |
| writer.writeLine(' filters: none') |
| writer.writeLine(' - pattern: /**') |
| writer.writeLine(' filters: JOINED_FILTERS') |
| 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<Map<String, String>> beans, String resourceConfigFilePath) { |
| final File resourceConfig = new File(resourceConfigFilePath) |
| List<String> 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()}" |
| } |
| } |
| } |
| |
| private void addBeansToPlugin(List<Map<String, String>> beans, File pluginClassFile) { |
| List<String> lines = [] |
| if (pluginClassFile.exists()) { |
| pluginClassFile.eachLine { line, nb -> |
| lines << line |
| if(line.trim().startsWith('package ')) { |
| beans.forEach(bean -> lines.add(bean.import)) |
| } |
| if (line.contains('doWithSpring()')) { |
| beans.each { Map bean -> |
| lines << ' ' + bean.definition |
| } |
| } |
| } |
| } |
| |
| pluginClassFile.withWriter('UTF-8') { writer -> |
| lines.each { String line -> |
| writer.write "${line}${System.lineSeparator()}" |
| } |
| } |
| } |
| |
| |
| private File findPluginClass() { |
| File pluginClass = null |
| new File("src/main/groovy").eachFileRecurse { fl -> |
| if (fl.isFile() && fl.name.endsWith("GrailsPlugin.groovy")) { |
| pluginClass = fl |
| } |
| } |
| return pluginClass |
| } |
| |
| } |
| |