Skip to content

Handle file permission errors in commands#1440

Merged
blackboxsw merged 8 commits into
canonical:mainfrom
aciba90:SC-658/query_userdata_root_perm
May 17, 2022
Merged

Handle file permission errors in commands#1440
blackboxsw merged 8 commits into
canonical:mainfrom
aciba90:SC-658/query_userdata_root_perm

Conversation

@aciba90
Copy link
Copy Markdown
Contributor

@aciba90 aciba90 commented May 9, 2022

Proposed Commit Message

Redact files with permission errors in commands.

Add tests covering this behavior for query, status and render
cmds.

Migrate `test_render.py` and `test_status.py` to Pytest.

LP: #1953430
SC-658

Additional Context

Test Steps

$ CONTAINER_NAME=some-ubuntu
$ lxc launch ubuntu-daily:jammy $CONTAINER_NAME
$ lxc exec $CONTAINER_NAME -- cloud-init status --wait
$ lxc exec $CONTAINER_NAME -- \
    chmod 400 '/etc/cloud/cloud.cfg.d/90_dpkg.cfg'

# Case 1
$ lxc exec $CONTAINER_NAME --user 1000 -- cloud-init query --all
<config with redacted parts>

# Case 2
$ lxc exec $CONTAINER_NAME --user 1000 -- cloud-init status --long
status: done
time: Tue, 10 May 2022 07:25:25 +0000
detail:
DataSourceLXD

Case 1 did exist with 1. Shows config with redacted parts.
Case 2 did raise an exception. Shows correct output. Log messages do not appear because of how the log is configured atm.

Checklist:

  • My code follows the process laid out in the documentation
  • I have updated or added any unit tests accordingly
  • I have updated or added any documentation accordingly

@aciba90 aciba90 marked this pull request as draft May 9, 2022 07:17
@aciba90 aciba90 marked this pull request as ready for review May 9, 2022 10:10
Copy link
Copy Markdown
Collaborator

@blackboxsw blackboxsw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR @aciba90 and for both pytest migrations and type hints.

While the solution you have cleans up the traceback, I think we might want to make the behavior of cloud-init status and cloud-init query still work for non-root users. Raising the exception prevents any non-root user from querying cloud-init status or cloud-init query until a root admin removes or changes perms on the offending files. If we can, let's instead emit REDACTED warnings for any configuration file that a non-root user doesn't have permission to that aligns with the messaging we currently have for cloud-init query vendordata for non-root. Given that cloud-init status and cloud-init query are really only trying to source Paths.run_dir the likelihood that a root-read-only file in /etc/cloud/cloud.cfg.d is overriding system_info: paths: is very slim to have an impact on the functionality of those tools.

As such, I'm thinking we can proceed with something more like the following:

diff --git a/cloudinit/util.py b/cloudinit/util.py
index 9fb0c6616..90c6c7a47 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -32,7 +32,7 @@ import subprocess
 import sys
 import time
 from base64 import b64decode, b64encode
-from errno import ENOENT
+from errno import EACCES, ENOENT
 from functools import lru_cache
 from typing import List
 from urllib import parse
@@ -1010,8 +1010,13 @@ def read_conf_d(confd):
     # Load them all so that they can be merged
     cfgs = []
     for fn in confs:
-        cfgs.append(read_conf(os.path.join(confd, fn)))
-
+        try:
+            cfgs.append(read_conf(os.path.join(confd, fn)))
+        except OSError as e:
+            if e.errno == EACCES:
+                LOG.warning(
+                    f"REDACTED config part {confd}/{fn} for non-root user"
+                )
     return mergemanydict(cfgs)

Additionally something I'd like to see in this PR is that if we get an error in cloud-init query --all due to permissions issues or some other unexpected error, we get no message printed to console, just and exit 1. I'd like us to handle exceptions and emit an appropriate error message in that event.

Comment thread cloudinit/cmd/status.py
Comment thread cloudinit/cmd/devel/__init__.py Outdated
init = Init(ds_deps=[])
init.read_cfg()
try:
init.read_cfg()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to raise an error in this case for non-root users. If we raise an error on any supplemental configuration files from /etc/cloud/cloud.cfg.d/* then non-root people/scripts running cloud-init status or cloud-init query will be unable to use that utility without root perms.

I believe we should probably align with the behavior we have in cloud-init query for non-root users. Any files to which you don't have permission get redacted with a comment/log or REDACTED label <redacted for non-root user> See cloud-init query vendordata or cloud-init query userdata when non-root user.

Given that cloud-init query and cloud-init status are read-only utilities for introspection of the system state and instance-data, I think we want to get as far as we can to allow consumers of those utilities without raising an error if possible. We can emit warnings about any config that is redacted to alert folks which have about configuration files which are ignored/redacted for non-root user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problematic files redacted.

@aciba90 aciba90 force-pushed the SC-658/query_userdata_root_perm branch from 4bc4603 to 1604081 Compare May 11, 2022 12:28
@aciba90 aciba90 marked this pull request as draft May 11, 2022 12:29
@aciba90
Copy link
Copy Markdown
Contributor Author

aciba90 commented May 11, 2022

Changed to redact configurations coming from files with permission errors.
Extra tests added to check that the sys-output is empty and that the log messages are correctly emitted.

Thank you for the comments and review. It is ready to be reviewed @blackboxsw

@aciba90 aciba90 marked this pull request as ready for review May 11, 2022 12:55
@blackboxsw blackboxsw self-assigned this May 16, 2022
Copy link
Copy Markdown
Collaborator

@blackboxsw blackboxsw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aciba90 a couple of notes on this behavior I think we may be able to drop a couple of the exception handling cases since you are coping with EACCES errors within read_conf_d.

Please let me know if I've misunderstood some elements.

Comment thread cloudinit/stages.py Outdated
Comment on lines +986 to +989
if e.errno == errno.EACCES:
pass # Already logged in `config_loaders`.
else:
raise
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a bit less code. Take if you feel like it or ignore if you don't.

Suggested change
if e.errno == errno.EACCES:
pass # Already logged in `config_loaders`.
else:
raise
if e.errno != errno.EACCES: # Already logged in config_loaders
raise

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread cloudinit/stages.py Outdated
)
config_loaders = [
# builtin config, hardcoded in settings.py.
util.get_builtin_cfg,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I appreciate the sentiment in the event we get permissions errors, the following callables will not generate EACCES errors so it might be over-engineering to cope with scenarios which won't raise EACCESS:

  • get_builtin_cfg (we are sourcing a static variable in the python code)
  • read_runtime_config pulls from /run/cloud-init which we set at world-readable
  • read_conf_from_cmdline pulls from /proc/cmdline which is also world-readable

The only function/callable that could generate that EACCES is read_conf_with_confd, so I don't think we need to add the layer of test/except handling here in a for loop if we handle this warning down in read_conf_with_confd

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After previous modifications, this is definitely not needed. Removed.

Comment thread cloudinit/util.py Outdated
except OSError as e:
if e.errno == EACCES:
LOG.warning("REDACTED config part %s for non-root user", cfgfile)
raise
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want a raise here, we want to proceed to other "conf_d" files if available.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked to proceed to other "conf_d files" if available.

Comment thread cloudinit/cmd/devel/render.py Outdated
Comment on lines +66 to +69
try:
paths = read_cfg_paths()
except (IOError, OSError):
return 1
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since init.read_cfg() no longer raises an OSError/IOError (and instead logs warnings) I don't think we need this try/except here anymore right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread cloudinit/stages.py


def read_runtime_config():
return util.read_conf(RUN_CLOUD_CONFIG)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloud-init owns and creates this file as world-readable, so I don't believe we need the try/except handling around this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, thanks.

aciba90 added 6 commits May 17, 2022 08:55
- Unify the way of reading cfg paths in status.py by using
`devel.read_cfg_paths`.
- Add error handling for `devel.read_cfg_paths`. It outputs an error
and sys-exists with 1.
- Add tests covering this behavior for query, status and render
cmds.
- Migrate `test_render.py` to Pytest.
- Remove error handling from stages.read_runtime_config as
/run/cloud-init/cloud.cfg is controlled by us.
- Rework read_conf_with_confd to work even if cfgfile is not
accessible by reading the confd directory only.
- Remove error handling from stages.fetch_base_config as
none of the functions raise.
- Same in render.handle_args.
- Adapt unit tests.
@aciba90 aciba90 force-pushed the SC-658/query_userdata_root_perm branch from 99d1c11 to f1ff067 Compare May 17, 2022 08:52
@aciba90 aciba90 force-pushed the SC-658/query_userdata_root_perm branch from ddf10bf to e0f9b78 Compare May 17, 2022 09:37
@aciba90
Copy link
Copy Markdown
Contributor Author

aciba90 commented May 17, 2022

Thanks for your suggestions and insights @blackboxsw . Now the code is much more simple. Ready to be reviewed.

Copy link
Copy Markdown
Collaborator

@blackboxsw blackboxsw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work on this PR and addressing all facets of the failure. Validated on server live install environments. LGTM

@blackboxsw blackboxsw merged commit 3e554d1 into canonical:main May 17, 2022
@aciba90 aciba90 deleted the SC-658/query_userdata_root_perm branch May 23, 2022 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants