Extend security state

   Implement management of:
     - CSRF protection
     - Content Security Policy
     - Agent to Master security

   Closes-PROD: https://mirantis.jira.com/browse/PROD-20183

Change-Id: I09439bbe534b84ad760091b7db471b5c07274a76
diff --git a/README.rst b/README.rst
index fc3c109..88e9eff 100644
--- a/README.rst
+++ b/README.rst
@@ -2,8 +2,8 @@
 Jenkins formula
 ===============
 
-Jenkins CI is an open source automation server written in Java. Jenkins 
-helps to automate the non-human part of software development process, with 
+Jenkins CI is an open source automation server written in Java. Jenkins
+helps to automate the non-human part of software development process, with
 continuous integration and facilitating technical aspects of continuous delivery.
 
 (*Source*: `Wikipedia <https://en.wikipedia.org/wiki/Jenkins_(software)>`_ )
@@ -509,7 +509,7 @@
         security:
           matrix:
             # set true for use ProjectMatrixAuthStrategy instead of GlobalMatrixAuthStrategy
-            project_based: false  
+            project_based: false
             permissions:
               Jenkins:
                 # administrator access
@@ -521,14 +521,14 @@
                   - user1
                   - user2
                 # agents permissions
-                MasterComputer: 
-                  BUILD: 
+                MasterComputer:
+                  BUILD:
                     - user3
               # jobs permissions
-              hudson: 
+              hudson:
                 model:
                   Item:
-                    BUILD: 
+                    BUILD:
                       - user4
 
 `Common matrix strategies <https://github.com/arbabnazar/configuration/blob/c08a5eaf4e04a68d2481375502a926517097b253/playbooks/roles/tools_jenkins/templates/projectBasedMatrixSecurity.groovy.j2>`_
@@ -697,7 +697,7 @@
 Slack plugin configuration
 
 .. code-block:: yaml
-    
+
     jenkins:
       client:
         slack:
@@ -735,15 +735,15 @@
 
 Jenkins Global env properties enforcing
 
- .. code-block:: yaml
+.. code-block:: yaml
 
-     jenkins:
-       client:
-         globalenvprop:
-           OFFLINE_DEPLOYMENT:
-             enabled: true
-             name: "OFFLINE_DEPLOYMENT" # optional, default using dict key
-             value: "true"
+    jenkins:
+      client:
+        globalenvprop:
+          OFFLINE_DEPLOYMENT:
+            enabled: true
+            name: "OFFLINE_DEPLOYMENT" # optional, default using dict key
+            value: "true"
 
 Throttle categories management from client (requires
 `Throttle Concurrent Builds <https://plugins.jenkins.io/throttle-concurrents>`_
@@ -825,6 +825,40 @@
             authkey: |
               SOMESSHKEY
 
+CSRF Protection configuration
+
+.. code-block:: yaml
+
+    jenkins:
+      client:
+        security:
+          csrf:
+            enabled: true
+            proxy_compat: false
+
+
+Agent to Master Access Control
+
+.. code-block:: yaml
+
+    jenkins:
+      client:
+        security:
+          agent2master:
+            enabled: true
+            whitelisted: ''
+            file_path_rules: ''
+
+Content Security Policy configuration
+
+.. code-block:: yaml
+
+    jenkins:
+      client:
+        security:
+          csp: "sandbox; default-src 'none'; img-src 'self'; style-src 'self';"
+
+
 Usage
 =====
 
diff --git a/_states/jenkins_security.py b/_states/jenkins_security.py
index e8389ba..ae4a496 100644
--- a/_states/jenkins_security.py
+++ b/_states/jenkins_security.py
@@ -14,6 +14,66 @@
     return True
 
 
+def agent2master(name, enabled, whitelisted, file_path_rules):
+    """
+    Jenkins Agent to Master Access Control state method
+
+    :param enabled
+    :param whitelisted: Whitelisted Commands
+    :param file_path_rules: File Access Rules
+    """
+
+    template = __salt__['jenkins_common.load_template'](
+        'salt://jenkins/files/groovy/security.agent2master.template',
+        __env__)
+    return __salt__['jenkins_common.api_call'](name, template,
+                        ["CHANGED", "EXISTS"],
+                        {
+                            'enabled': enabled,
+                            'newWhitelistedContent': whitelisted,
+                            'newFilePathRulesContent': file_path_rules
+                        },
+                        'Agent to Master Access Control')
+
+def csp(name, policy):
+    """
+    Jenkins Content Security Policy state method
+
+    :param policy
+    """
+    default_policy = """\
+            sandbox; default-src 'none'; img-src 'self'; style-src 'self';
+        """.strip()
+
+    template = __salt__['jenkins_common.load_template'](
+        'salt://jenkins/files/groovy/security.csp.template',
+        __env__)
+    return __salt__['jenkins_common.api_call'](name, template,
+                        ["CHANGED", "EXISTS"],
+                        {
+                            'policy': policy if policy else default_policy,
+                        },
+                        'Content Security Policy')
+
+def csrf(name, enabled, proxy_compat):
+    """
+    Jenkins CSRF protection state method
+
+    :param enabled
+    :param proxy_compat
+    """
+
+    template = __salt__['jenkins_common.load_template'](
+        'salt://jenkins/files/groovy/security.csrf.template',
+        __env__)
+    return __salt__['jenkins_common.api_call'](name, template,
+                        ["CHANGED", "EXISTS"],
+                        {
+                            'csrfEnabled': enabled,
+                            'proxyCompat': proxy_compat
+                        },
+                        'CSRF Protection')
+
 def ldap(name, server, root_dn, user_search_base, manager_dn, manager_password,
          user_search="", group_search_base="", inhibit_infer_root_dn=False):
     """
diff --git a/jenkins/client/security.sls b/jenkins/client/security.sls
index 18bba8f..4b9cc41 100644
--- a/jenkins/client/security.sls
+++ b/jenkins/client/security.sls
@@ -17,4 +17,28 @@
   jenkins_security.matrix:
     - strategies: {{ client.security.matrix.permissions }}
     - project_based: {{ client.security.matrix.get('project_based', False) }}
-{%- endif %}
\ No newline at end of file
+{%- endif %}
+
+{%- if client.security.csrf is defined %}
+jenkins_csrf_protection:
+  jenkins_security.csrf:
+    - enabled: {{ client.security.csrf.get('enabled', False) }}
+    - proxy_compat: {{ client.security.csrf.get('proxy_compat', False) }}
+{%- endif %}
+
+{%- if client.security.csp is defined %}
+jenkins_content_security_policy:
+  jenkins_security.csp:
+    - policy: {{ client.security.csp }}
+{%- endif %}
+
+{%- if client.security.agent2master is defined %}
+jenkins_agent2master_security:
+  jenkins_security.agent2master:
+    - enabled: {{ client.security.agent2master.get('enabled', False) }}
+    - whitelisted: |
+        {{ client.security.agent2master.get('whitelisted', '')|indent(8) }}
+    - file_path_rules: |
+        {{ client.security.agent2master.get('file_path_rules', '')|indent(8) }}
+{%- endif %}
+
diff --git a/jenkins/files/groovy/security.agent2master.template b/jenkins/files/groovy/security.agent2master.template
new file mode 100644
index 0000000..b1f7b2c
--- /dev/null
+++ b/jenkins/files/groovy/security.agent2master.template
@@ -0,0 +1,52 @@
+#!groovy
+import jenkins.model.Jenkins
+import jenkins.security.s2m.*
+
+Boolean enabled = "${enabled}".toBoolean()
+
+String newWhitelistedContent="""\
+${newWhitelistedContent}"""
+
+String newFilePathRulesContent="""\
+${newFilePathRulesContent}"""
+
+Boolean changed = false
+
+/*
+ * MasterKillSwitch has reversed logic
+ * `true` means `disabled` and vise versa
+ */
+
+def instance = Jenkins.getInstance()
+def whitelistRule = instance.injector.getInstance(AdminWhitelistRule.class)
+Boolean s2mState = !whitelistRule.getMasterKillSwitch() //reversed logic. `true` means disabled
+
+if ( s2mState != enabled ) {
+    whitelistRule.setMasterKillSwitch(!enabled)
+    changed = true
+}
+
+String whitelistedFile = whitelistRule.whitelisted.toString()
+String filePathRulesFile = whitelistRule.filePathRules.toString()
+String whitelistedContent = new File(whitelistedFile).getText('UTF-8')
+String filePathRulesContent = new File(filePathRulesFile).getText('UTF-8')
+
+if (!newWhitelistedContent.endsWith("\n"))
+     newWhitelistedContent+="\n"
+
+if (newWhitelistedContent != whitelistedContent) {
+    whitelistRule.whitelisted.set(newWhitelistedContent);
+    changed = true
+}
+
+if (newFilePathRulesContent != filePathRulesContent) {
+    whitelistRule.filePathRules.parseTest(newFilePathRulesContent)
+    whitelistRule.filePathRules.set(newFilePathRulesContent);
+    changed = true
+}
+
+if (changed) {
+    print "CHANGED"
+} else {
+    print "EXISTS"
+}
diff --git a/jenkins/files/groovy/security.csp.template b/jenkins/files/groovy/security.csp.template
new file mode 100644
index 0000000..51c794c
--- /dev/null
+++ b/jenkins/files/groovy/security.csp.template
@@ -0,0 +1,10 @@
+#!groovy
+
+def currentPolicy = System.getProperty("hudson.model.DirectoryBrowserSupport.CSP")
+def newPolicy = """${policy}""".trim()
+if ( currentPolicy != newPolicy ){
+    System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", newPolicy)
+    print ("CHANGED")
+} else {
+    print ("EXISTS")
+}
diff --git a/jenkins/files/groovy/security.csrf.template b/jenkins/files/groovy/security.csrf.template
new file mode 100644
index 0000000..4d8ef1f
--- /dev/null
+++ b/jenkins/files/groovy/security.csrf.template
@@ -0,0 +1,34 @@
+#!groovy
+
+import hudson.security.csrf.DefaultCrumbIssuer
+import jenkins.model.Jenkins
+
+Boolean proxyCompat = "${proxyCompat}".toBoolean()
+Boolean csrfEnabled = "${csrfEnabled}".toBoolean()
+
+def instance = Jenkins.getInstance()
+def issuer = instance.getCrumbIssuer()
+
+if (csrfEnabled) {
+  if (! issuer ) {
+      instance.setCrumbIssuer(new DefaultCrumbIssuer(proxyCompat))
+      instance.save()
+      print "CHANGED"
+  } else {
+      if ( issuer.excludeClientIPFromCrumb != proxyCompat ) {
+          issuer.excludeClientIPFromCrumb = proxyCompat
+          instance.save()
+          print "CHANGED"
+      } else {
+          print "EXISTS"
+      }
+  }
+} else {
+  if ( issuer ) {
+      instance.setCrumbIssuer(null)
+      instance.save()
+      print "CHANGED"
+  } else {
+    print "EXISTS"
+  }
+}