Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@
"content" : {
"type" : [ "object", "null" ],
"properties" : {
"allowedStaticsWhitelist" : {
"description" : "Whitelist for Statics freemarker is allowing in user templates",
"type" : [ "array", "null" ],
"items" : {
"type" : "string"
}
},
"examples" : {
"type" : [ "boolean", "null" ],
"description" : "Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project"
Expand Down Expand Up @@ -230,6 +237,10 @@
"additionalProperties" : false
}
},
"useWhitelist" : {
"type" : [ "boolean", "null" ],
"description" : "Enables the whitelist for statics in content templating"
},
"variables" : {
"$ref" : "#/$defs/Map(String,Object)-nullable",
"description" : "Additional variables to use in custom templates."
Expand Down
6 changes: 3 additions & 3 deletions docs/content-loader/content-loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ image:
tag : ${operatorImageObject.tag}
</#if>
```

By default, Freemarker grants access to all static resources within the project. To add an extra layer of security, set the content.useWhitelist property to true in the GOP configuration, or use the --content-whitelist CLI flag to enable the static resources whitelist.
To specify which static resources should be accessible, add them to the allowedStaticsWhitelist in the configuration. A default set of static resources is already provided as an example.
# TL;DR

How to get started with content loader?
Expand Down Expand Up @@ -323,5 +324,4 @@ Reminder: no type means `MIRROR` (default).
templating: true
type: FOLDER_BASED
overrideMode: UPGRADE
```

```
16 changes: 16 additions & 0 deletions src/main/groovy/com/cloudogu/gitops/config/Config.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ class Config {
@JsonPropertyDescription(CONTENT_VARIABLES_DESCRIPTION)
Map<String, Object> variables = [:]

@Option(names = ['--content-whitelist'], description = CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
@JsonPropertyDescription(CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
Boolean useWhitelist = false

@JsonPropertyDescription(CONTENT_STATICSWHITELIST_DESCRIPTION)
Set<String> allowedStaticsWhitelist = [
'java.lang.String',
'java.lang.Integer',
'java.lang.Long',
'java.lang.Double',
'java.lang.Float',
'java.lang.Boolean',
'java.lang.Math',
'com.cloudogu.gitops.utils.DockerImageParser'
] as Set<String>

static class ContentRepositorySchema {
static final String DEFAULT_PATH = '.'
// This is controversial. Forcing users to explicitly choose a type requires them to understand the concept
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ interface ConfigConstants {
String CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION = "This defines, how customer repos will be updated.\nINIT - push only if repo does not exist.\nRESET - delete all files after cloning source - files not in content are deleted\nUPGRADE - clone and copy - existing files will be overwritten, files not in content are kept. For type: MIRROR reset and upgrade have same result: in both cases source repo will be force pushed to target repo."
String CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION = "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches."
String CONTENT_VARIABLES_DESCRIPTION = "Additional variables to use in custom templates."
String CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION = 'Enables the whitelist for statics in content templating'
String CONTENT_STATICSWHITELIST_DESCRIPTION = 'Whitelist for Statics freemarker is allowing in user templates'

// group jenkins
String JENKINS_ENABLE_DESCRIPTION = 'Installs Jenkins as CI server'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.cloudogu.gitops.config.Config.OverwriteMode
import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepo
import com.cloudogu.gitops.git.GitRepoFactory
import com.cloudogu.gitops.utils.AllowListFreemarkerObjectWrapper
import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.utils.K8sClient
import com.cloudogu.gitops.utils.TemplatingEngine
Expand Down Expand Up @@ -229,7 +230,8 @@ class ContentLoader extends Feature {
engine.replaceTemplates(srcPath, [
config : config,
// Allow for using static classes inside the templates
statics: new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels()
statics: !config.content.useWhitelist ? new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels() :
new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, config.content.getAllowedStaticsWhitelist()).getStaticModels()
])
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.cloudogu.gitops.utils

import freemarker.template.*

class AllowListFreemarkerObjectWrapper extends DefaultObjectWrapper {

Set<String> allowlist

AllowListFreemarkerObjectWrapper(Version freemarkerVersion, Set<String> allowlist) {
super(freemarkerVersion)
this.allowlist = allowlist
}

TemplateHashModel getStaticModels() {
final TemplateHashModel originalStaticModels = super.getStaticModels()
final Set<String> allowlistCopy = this.allowlist

return new TemplateHashModel() {
@Override
TemplateModel get(String key) throws TemplateModelException {
if (allowlistCopy.contains(key)) {
return originalStaticModels.get(key)
}
return null
}

@Override
boolean isEmpty() throws TemplateModelException {
return allowlistCopy.isEmpty()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.cloudogu.gitops.utils

import freemarker.template.Configuration
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.*

class AllowlistFreemarkerObjectWrapperTest {


@Test
void 'should allow access to whitelisted static models'() {
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["com.cloudogu.gitops.utils.DockerImageParser"] as Set)
def staticModels = wrapper.getStaticModels()

assertNotNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser"))
assertNull(staticModels.get("java.lang.Integer"))
assertNull(staticModels.get("java.lang.String"))
}

@Test
void 'should deny access to non-whitelisted static models'() {
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["java.lang.String"] as Set)
def staticModels = wrapper.getStaticModels()

assertNull(staticModels.get("java.lang.Integer"))
assertNotNull(staticModels.get("java.lang.String"))
assertNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser"))
}

@Test
void 'should return true for isEmpty when allowlist is empty'() {
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, [] as Set)
def staticModels = wrapper.getStaticModels()

assertTrue(staticModels.isEmpty())
}

@Test
void 'templating only works for whitelisted statics'() {
def templateText = '''
<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']>
<#assign imageObject = DockerImageParser.parse('test:latest')>
<#assign staticsTests=statics['System']>
<#assign imageObject = staticsTests.exit()>
'''.stripIndent()

def model = [
statics: new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ['com.cloudogu.gitops.utils.DockerImageParser'] as Set).getStaticModels()
] as Map<String, Object>
// create a temporary file to simulate an actual file input
def tempInputFile = File.createTempFile("test", ".ftl.yaml")
tempInputFile.text = templateText

def exception = assertThrows(freemarker.core.InvalidReferenceException) {
new TemplatingEngine().replaceTemplates(tempInputFile, model)
}

assert exception.message.contains("System") : "Exception message should mention 'System'"
}

@Test
void 'templating in ftl files works correctly with whitelisted static models'() {
def templateText = '''
<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']>
<#assign imageObject = DockerImageParser.parse('test:latest')>
<#assign staticsTests=statics['java.lang.Math']>
<#assign number = staticsTests.round(3.14)>
'''.stripIndent()

def model = [
statics: new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ['java.lang.Math', 'com.cloudogu.gitops.utils.DockerImageParser'] as Set).getStaticModels()
] as Map<String, Object>
// create a temporary file to simulate an actual file input
def tempInputFile = File.createTempFile("test", ".ftl.yaml")
tempInputFile.text = templateText

new TemplatingEngine().replaceTemplates(tempInputFile, model)

}
}