sanitize path symbols in inventory_hostname on file cache plugins (#41420)

* File based cache plugins filenames fix

File based cache plugins will now correctly handle inventory_hostnames
with 'path symbols' in their names. This should allow those using
chroot and jail connection plugins to use file based caches now.
pull/86077/head
Brian Coca 1 month ago committed by GitHub
parent 7bd2475a70
commit d9d11d6ff6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,2 @@
bugfixes:
- cache plugins based on the BaseFileCache class will now sanitize keys to avoid names that could cause issues with the storage path

@ -18,6 +18,7 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import hashlib
import os import os
import tempfile import tempfile
import time import time
@ -40,6 +41,8 @@ display = Display()
class BaseCacheModule(AnsiblePlugin): class BaseCacheModule(AnsiblePlugin):
_PATH_CHARS = frozenset({'/', '..', '<', '>', '|'})
# Backwards compat only. Just import the global display instead # Backwards compat only. Just import the global display instead
_display = display _display = display
_persistent = True _persistent = True
@ -91,6 +94,8 @@ class BaseFileCacheModule(BaseCacheModule):
self.plugin_name = resource_from_fqcr(self.__module__) self.plugin_name = resource_from_fqcr(self.__module__)
self._cache = {} self._cache = {}
self.validate_cache_connection() self.validate_cache_connection()
self._sanitized = {}
self._files = {}
def _get_cache_connection(self, source): def _get_cache_connection(self, source):
if source: if source:
@ -99,10 +104,23 @@ class BaseFileCacheModule(BaseCacheModule):
except TypeError: except TypeError:
pass pass
def _sanitize_key(self, key: str) -> str:
"""
Ensures key name is safe to use on the filesystem
"""
if key not in self._sanitized:
for invalid in self._PATH_CHARS:
if invalid in key:
self._sanitized[key] = hashlib.sha256(key.encode()).hexdigest()[:max(len(key), 12)]
break
else:
self._sanitized[key] = key
return self._sanitized[key]
def validate_cache_connection(self): def validate_cache_connection(self):
if not self._cache_dir: if not self._cache_dir:
raise AnsibleError("error, '%s' cache plugin requires the 'fact_caching_connection' config option " raise AnsibleError(f"'{self.plugin_name!r}' cache plugin requires the 'fact_caching_connection' configuration option "
"to be set (to a writeable directory path)" % self.plugin_name) "to be set (to a writeable directory path)")
if not os.path.exists(self._cache_dir): if not os.path.exists(self._cache_dir):
try: try:
@ -112,16 +130,17 @@ class BaseFileCacheModule(BaseCacheModule):
else: else:
for x in (os.R_OK, os.W_OK, os.X_OK): for x in (os.R_OK, os.W_OK, os.X_OK):
if not os.access(self._cache_dir, x): if not os.access(self._cache_dir, x):
raise AnsibleError("error in '%s' cache, configured path (%s) does not have necessary permissions (rwx), disabling plugin" % ( raise AnsibleError(f"'{self.plugin_name!r}' cache, configured path ({self._cache_dir}) does not have necessary permissions (rwx),"
self.plugin_name, self._cache_dir)) " disabling plugin")
def _get_cache_file_name(self, key: str) -> str: def _get_cache_file_name(self, key: str) -> str:
prefix = self.get_option('_prefix') if key not in self._files:
if prefix: safe = self._sanitize_key(key) # use key or filesystem safe hash of key
cachefile = "%s/%s%s" % (self._cache_dir, prefix, key) prefix = self.get_option('_prefix')
else: if not prefix:
cachefile = "%s/%s" % (self._cache_dir, key) prefix = ''
return cachefile self._files[key] = os.path.join(self._cache_dir, prefix + safe)
return self._files[key]
def get(self, key): def get(self, key):
""" This checks the in memory cache first as the fact was not expired at 'gather time' """ This checks the in memory cache first as the fact was not expired at 'gather time'
@ -155,7 +174,7 @@ class BaseFileCacheModule(BaseCacheModule):
self._cache[key] = value self._cache[key] = value
cachefile = self._get_cache_file_name(key) cachefile = self._get_cache_file_name(key)
tmpfile_handle, tmpfile_path = tempfile.mkstemp(dir=os.path.dirname(cachefile)) tmpfile_handle, tmpfile_path = tempfile.mkstemp(dir=self._cache_dir)
try: try:
try: try:
self._dump(value, tmpfile_path) self._dump(value, tmpfile_path)

@ -0,0 +1,49 @@
from __future__ import annotations
DOCUMENTATION = """
name: dummy_file_cache
short_description: dummy file cache
description: see short
options:
_uri:
required: True
description:
- Path in which the cache plugin will save the files
env:
- name: ANSIBLE_CACHE_PLUGIN_CONNECTION
ini:
- key: fact_caching_connection
section: defaults
type: path
_prefix:
description: User defined prefix to use when creating the files
env:
- name: ANSIBLE_CACHE_PLUGIN_PREFIX
ini:
- key: fact_caching_prefix
section: defaults
_timeout:
default: 86400
description: Expiration timeout for the cache plugin data
env:
- name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
ini:
- key: fact_caching_timeout
section: defaults
type: integer
"""
from ansible.plugins.cache import BaseFileCacheModule
class CacheModule(BaseFileCacheModule):
_persistent = False
def _load(self, filepath: str) -> object:
with open(filepath, 'r') as jfile:
return eval(filepath.read())
def _dump(self, value: object, filepath: str) -> None:
with open(filepath, 'w') as afile:
afile.write(str(value))

@ -0,0 +1,47 @@
from __future__ import annotations
DOCUMENTATION = """
name: dummy_file_cache
short_description: dummy file cache
description: see short
options:
_uri:
required: True
description:
- Path in which the cache plugin will save the files
env:
- name: ANSIBLE_CACHE_PLUGIN_CONNECTION
ini:
- key: fact_caching_connection
section: defaults
type: path
_prefix:
description: User defined prefix to use when creating the files
env:
- name: ANSIBLE_CACHE_PLUGIN_PREFIX
ini:
- key: fact_caching_prefix
section: defaults
_timeout:
default: 86400
description: Expiration timeout for the cache plugin data
env:
- name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
ini:
- key: fact_caching_timeout
section: defaults
type: integer
"""
from ansible.plugins.cache import BaseFileCacheModule
class CacheModule(BaseFileCacheModule):
def _load(self, filepath: str) -> object:
with open(filepath, 'r') as jfile:
return eval(filepath.read())
def _dump(self, value: object, filepath: str) -> None:
with open(filepath, 'w') as afile:
afile.write(str(value))

@ -0,0 +1,14 @@
chroots:
hosts:
/my/chroot/host1:
bogusvar: foobarvalue
/my/chroot/host2:
traversal:
hosts:
..:
...:
all:
vars:
ansible_connection: local
ansible_python_interpreter: '{{ansible_playbook_python}}'

@ -0,0 +1,7 @@
- hosts: all
gather_facts: false
tasks:
- name: populate cache, will fail if invalid files are used
set_fact:
cacheable: true
testing: 123{{inventory_hostname}}

@ -30,3 +30,17 @@ export ANSIBLE_INVENTORY_CACHE_PLUGIN=dummy_cache
ansible-playbook test_inventory_cache.yml "$@" ansible-playbook test_inventory_cache.yml "$@"
ansible-playbook inspect_inventory_cache.yml -i test.inventoryconfig.yml "$@" ansible-playbook inspect_inventory_cache.yml -i test.inventoryconfig.yml "$@"
# test file based cache with 'fun' inventory names
export ANSIBLE_CACHE_PLUGIN=dummy_file_cache ANSIBLE_CACHE_PLUGIN_CONNECTION="${OUTPUT_DIR}/dummy-file-cache"
mkdir -p "${ANSIBLE_CACHE_PLUGIN_CONNECTION}"
ansible-playbook -i chroot_inventory_config.yml invalid_hostname_file_caches.yml "$@"
# same, but using 'persistent' route
ANSIBLE_CACHE_PLUGIN=dummy_file_cache_persistent ansible-playbook -i chroot_inventory_config.yml invalid_hostname_file_caches.yml "$@"
# test file based cache with 'fun' inventory names, and a prefix!
export ANSIBLE_CACHE_PLUGIN_PREFIX="YOLO"
ansible-playbook -i chroot_inventory_config.yml invalid_hostname_file_caches.yml "$@"
ANSIBLE_CACHE_PLUGIN=dummy_file_cache_persistent ansible-playbook -i chroot_inventory_config.yml invalid_hostname_file_caches.yml "$@"

Loading…
Cancel
Save