Manage /etc/{at,cron}.{allow,deny} files

Related-Prod: PROD-22546
Related-Prod: PROD-22664

Change-Id: I66a35ef3d2436541ef70f02e2631fa8d4d86e5e9
diff --git a/README.rst b/README.rst
index 986ec72..50e1b13 100644
--- a/README.rst
+++ b/README.rst
@@ -235,7 +235,54 @@
           automatic_reboot: true
           automatic_reboot_time: "02:00"
 
-Linux with cron jobs
+Managing cron tasks
+-------------------
+
+There are two data structures that are related to managing cron itself and
+cron tasks:
+
+.. code-block:: yaml
+
+    linux:
+      system:
+        cron:
+
+and
+
+.. code-block:: yaml
+
+    linux:
+      system:
+        job:
+
+`linux:system:cron` manages cron packages, services, and '/etc/cron.allow' file.
+
+'deny' files are managed the only way - we're ensuring they are absent, that's
+a requirement from CIS 5.1.8
+
+'cron' pillar structure is the following:
+
+.. code-block:: yaml
+
+    linux:
+      system:
+        cron:
+          enabled: true
+          pkgs: [ <cron packages> ]
+          services: [ <cron services> ]
+          user:
+            <username>:
+              enabled: true
+
+To add user to '/etc/cron.allow' use 'enabled' key as shown above.
+
+'/etc/cron.deny' is not managed as CIS 5.1.8 requires it was removed.
+
+A user would be ignored if any of the following is true:
+* user is disabled in `linux:system:user:<username>`
+* user is disabled in `linux:system:cron:user:<username>`
+
+`linux:system:job` manages individual cron tasks.
 
 By default, it will use name as an identifier, unless identifier key is
 explicitly set or False (then it will use Salt's default behavior which is
@@ -255,6 +302,32 @@
             hour: 2
             minute: 0
 
+Managing 'at' tasks
+-------------------
+
+Pillar for managing `at` tasks is similar to one for `cron` tasks:
+
+.. code-block:: yaml
+
+    linux:
+      system:
+        at:
+          enabled: true
+          pkgs: [ <at packages> ]
+          services: [ <at services> ]
+          user:
+            <username>:
+              enabled: true
+
+To add a user to '/etc/at.allow' use 'enabled' key as shown above.
+
+'/etc/at.deny' is not managed as CIS 5.1.8 requires it was removed.
+
+A user will be ignored if any of the following is true:
+* user is disabled in `linux:system:user:<username>`
+* user is disabled in `linux:system:at:user:<username>`
+
+
 Linux security limits (limit sensu user memory usage to max 1GB):
 
 .. code-block:: yaml
diff --git a/linux/files/cron_users.jinja b/linux/files/cron_users.jinja
new file mode 100644
index 0000000..fe47059
--- /dev/null
+++ b/linux/files/cron_users.jinja
@@ -0,0 +1,5 @@
+# This file is managed by Salt, do not edit
+{%- for user_name in users %}
+{{ user_name }}
+{%- endfor %}
+{# IMPORTANT: This file SHOULD ends with a newline #}
\ No newline at end of file
diff --git a/linux/map.jinja b/linux/map.jinja
index c333a89..52d0e70 100644
--- a/linux/map.jinja
+++ b/linux/map.jinja
@@ -85,6 +85,24 @@
     },
 }, grain='os_family', merge=salt['pillar.get']('linux:system')) %}
 
+{% set at = salt['grains.filter_by']({
+    'Debian': {
+        'enabled': false,
+        'pkgs': ['at'],
+        'services': ['atd'],
+        'user': {}
+    },
+}, grain='os_family', merge=salt['pillar.get']('linux:system:at')) %}
+
+{% set cron = salt['grains.filter_by']({
+    'Debian': {
+        'enabled': false,
+        'pkgs': ['cron'],
+        'services': ['cron'],
+        'user': {}
+    },
+}, grain='os_family', merge=salt['pillar.get']('linux:system:cron')) %}
+
 {% set banner = salt['grains.filter_by']({
     'BaseDefaults': {
         'enabled': false,
diff --git a/linux/system/at.sls b/linux/system/at.sls
new file mode 100644
index 0000000..a441d1a
--- /dev/null
+++ b/linux/system/at.sls
@@ -0,0 +1,62 @@
+{%- from "linux/map.jinja" import system, at with context %}
+
+{%- if at.get('enabled', false) %}
+
+at_packages:
+  pkg.installed:
+    - names: {{ at.pkgs }}
+
+at_services:
+  service.running:
+    - enable: true
+    - names: {{ at.services }}
+    - require:
+      - pkg: at_packages
+  {%- if grains.get('noservices') %}
+    - onlyif: /bin/false
+  {%- endif %}
+
+  {%- set allow_users = [] %}
+  {%- for user_name, user_params in at.get('user', {}).items() %}
+    {%- set user_enabled = user_params.get('enabled', false) and
+        system.get('user', {}).get(
+          user_name, {'enabled': true}).get('enabled', true) %}
+    {%- if user_enabled %}
+      {%- do allow_users.append(user_name) %}
+    {%- endif %}
+  {%- endfor %}
+
+etc_at_allow:
+  {%- if allow_users %}
+  file.managed:
+    - name: /etc/at.allow
+    - template: jinja
+    - source: salt://linux/files/cron_users.jinja
+    - user: root
+    - group: root
+    - mode: 0600
+    - defaults:
+        users: {{ allow_users | yaml }}
+    - require:
+      - cron_packages
+  {%- else %}
+  file.absent:
+    - name: /etc/at.allow
+  {%- endif %}
+
+
+{#
+    /etc/at.deny should be absent to comply with
+    CIS 5.1.8 Ensure at/cron is restricted to authorized users
+#}
+etc_at_deny:
+  file.absent:
+    - name: /etc/at.deny
+
+{%- else %}
+
+fake_linux_system_at:
+  test.nop:
+    - comment: Fake state to satisfy 'require sls:linux.system.at'
+
+{%- endif %}
diff --git a/linux/system/cron.sls b/linux/system/cron.sls
new file mode 100644
index 0000000..7f7ae0e
--- /dev/null
+++ b/linux/system/cron.sls
@@ -0,0 +1,87 @@
+{%- from "linux/map.jinja" import system, cron with context %}
+
+{%- if cron.get('enabled', false) %}
+
+cron_packages:
+  pkg.installed:
+    - names: {{ cron.pkgs }}
+
+cron_services:
+  service.running:
+    - enable: true
+    - names: {{ cron.services }}
+    - require:
+      - pkg: cron_packages
+  {%- if grains.get('noservices') %}
+    - onlyif: /bin/false
+  {%- endif %}
+
+  {%- set allow_users = [] %}
+  {%- for user_name, user_params in cron.get('user', {}).items() %}
+    {%- set user_enabled = user_params.get('enabled', false) and
+        system.get('user', {}).get(
+          user_name, {'enabled': true}).get('enabled', true) %}
+    {%- if user_enabled %}
+      {%- do allow_users.append(user_name) %}
+    {%- endif %}
+  {%- endfor %}
+
+etc_cron_allow:
+  {%- if allow_users %}
+  file.managed:
+    - name: /etc/cron.allow
+    - template: jinja
+    - source: salt://linux/files/cron_users.jinja
+    - user: root
+    - group: root
+    - mode: 0600
+    - defaults:
+        users: {{ allow_users | yaml }}
+    - require:
+      - cron_packages
+  {%- else %}
+  file.absent:
+    - name: /etc/cron.allow
+  {%- endif %}
+
+{#
+    /etc/cron.deny should be absent to comply with
+    CIS 5.1.8 Ensure at/cron is restricted to authorized users
+#}
+etc_cron_deny:
+  file.absent:
+    - name: /etc/cron.deny
+
+etc_crontab:
+  file.managed:
+    - name: /etc/crontab
+    - user: root
+    - group: root
+    - mode: 0600
+    - replace: False
+    - require:
+      - cron_packages
+
+etc_cron_dirs:
+  file.directory:
+    - names:
+      - /etc/cron.d
+      - /etc/cron.daily
+      - /etc/cron.hourly
+      - /etc/cron.monthly
+      - /etc/cron.weekly
+    - user: root
+    - group: root
+    - dir_mode: 0600
+    - recurse:
+      - ignore_files
+    - require:
+      - cron_packages
+
+{%- else %}
+
+fake_linux_system_cron:
+  test.nop:
+    - comment: Fake state to satisfy 'require sls:linux.system.cron'
+
+{%- endif %}
diff --git a/linux/system/init.sls b/linux/system/init.sls
index ad3681a..4f97fa0 100644
--- a/linux/system/init.sls
+++ b/linux/system/init.sls
@@ -3,6 +3,8 @@
 include:
 - linux.system.env
 - linux.system.profile
+- linux.system.at
+- linux.system.cron
 {%- if system.repo|length > 0 %}
 - linux.system.repo
 {%- endif %}
diff --git a/linux/system/job.sls b/linux/system/job.sls
index af42b58..4cdb946 100644
--- a/linux/system/job.sls
+++ b/linux/system/job.sls
@@ -3,45 +3,46 @@
 
 include:
 - linux.system.user
+- linux.system.cron
 
-{%- for name, job in system.job.items() %}
+  {%- for name, job in system.job.items() %}
+    {%- set job_user = job.get('user', 'root') %}
 
 linux_job_{{ job.command }}:
-  {%- if job.enabled|default(True) %}
+    {%- if job.get('enabled', True) %}
   cron.present:
     - name: >
         {{ job.command }}
-    {%- if job.get('identifier', True) %}
+      {%- if job.get('identifier', True) %}
     - identifier: {{ job.get('identifier', job.get('name', name)) }}
-    {%- endif %}
-    - user: {{ job.user|default("root") }}
-    {%- if job.minute is defined %}
+      {%- endif %}
+    - user: {{ job_user }}
+      {%- if job.minute is defined %}
     - minute: '{{ job.minute }}'
-    {%- endif %}
-    {%- if job.hour is defined %}
+      {%- endif %}
+      {%- if job.hour is defined %}
     - hour: '{{ job.hour }}'
-    {%- endif %}
-    {%- if job.daymonth is defined %}
+      {%- endif %}
+      {%- if job.daymonth is defined %}
     - daymonth: '{{ job.daymonth }}'
-    {%- endif %}
-    {%- if job.month is defined %}
+      {%- endif %}
+      {%- if job.month is defined %}
     - month: '{{ job.month }}'
-    {%- endif %}
-    {%- if job.dayweek is defined %}
+      {%- endif %}
+      {%- if job.dayweek is defined %}
     - dayweek: '{{ job.dayweek }}'
-    {%- endif %}
-    {%- if job.user|default("root") in system.get('user', {}).keys() %}
+      {%- endif %}
     - require:
-      - user: system_user_{{ job.user|default("root") }}
-    {%- endif %}
-  {%- else %}
+      - sls: linux.system.cron
+      {%- if job_user in system.get('user', {}).keys() %}
+      - user: system_user_{{ job_user }}
+      {%- endif %}
+    {%- else %}
   cron.absent:
     - name: {{ job.command }}
-    {%- if job.get('identifier', True) %}
+      {%- if job.get('identifier', True) %}
     - identifier: {{ job.get('identifier', job.get('name', name)) }}
+      {%- endif %}
     {%- endif %}
-  {%- endif %}
-
-{%- endfor %}
-
+  {%- endfor %}
 {%- endif %}
diff --git a/tests/pillar/system.sls b/tests/pillar/system.sls
index 555a0cd..0b792b6 100644
--- a/tests/pillar/system.sls
+++ b/tests/pillar/system.sls
@@ -5,6 +5,20 @@
     fqdn: linux.ci.local
   system:
     enabled: true
+    at:
+      enabled: true
+      user:
+        root:
+          enabled: true
+        testuser:
+          enabled: true
+    cron:
+      enabled: true
+      user:
+        root:
+          enabled: true
+        testuser:
+          enabled: true
     cluster: default
     name: linux
     domain: ci.local