plugin-core/plugin/grails-app/commands/grails.plugin.springsecurity/S2QuickstartCommand.groovy (244 lines of code) (raw):
/*
* 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>./gradlew runCommand "-Pargs=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>./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role --groupClassName=RoleGroup"</code>
* 2. <code>./gradlew runCommand "-Pargs=s2-quickstart com.yourapp Person Authority Requestmap"</code>
* 3. <code>./gradlew runCommand "-Pargs=s2-quickstart --uiOnly"</code>
* 4. <code>./gradlew runCommand "-Pargs=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 = '''
./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<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()])
addBeans(beans, 'grails-app/conf/spring/resources.groovy')
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<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()}"
}
}
}
}