Skip to content

fix: use plain open() when creating empty config files#260

Merged
baxeaz merged 1 commit into
OpenJobDescription:mainlinefrom
baxeaz:fix/config-manager-skip-secure-open
May 25, 2026
Merged

fix: use plain open() when creating empty config files#260
baxeaz merged 1 commit into
OpenJobDescription:mainlinefrom
baxeaz:fix/config-manager-skip-secure-open

Conversation

@baxeaz
Copy link
Copy Markdown
Contributor

@baxeaz baxeaz commented May 22, 2026

Fixes: N/A — no tracking issue

What was the problem/requirement? (What/Why)

ConfigurationManager._ensure_config_file creates an empty <AdaptorName>.json on first access if one does not yet exist. That create path goes through secure_open(), which on Windows replaces the file's DACL with a single non-inheriting ACE granting only the current owner SID FILE_GENERIC_READ | FILE_GENERIC_WRITE | DELETE (see set_file_permissions_in_windows in _secure_open.py).

The effect is that the config file's ACL stops following the parent directory's inheritable ACLs and is permanently bound to the SID of whichever account happened to first create it. If that file is later accessed by any other account — even one that would have had inherited access from the parent directory — the open fails with PermissionError. It also runs counter to what most users will reasonably expect from filesystem permissions on an ordinary config file.

There is no documented requirement for adaptor config files to be owner-only at the file level. The directories these files live in (<adaptor module dir>/, ~/.openjd/adaptors/<name>/, %PROGRAMDATA%\openjd\adaptors\<name>\) already restrict access via their own ACLs.

What was the solution? (How)

Switch the create path in _ensure_config_file to use plain open(filepath, mode="w", encoding="utf-8") and let normal filesystem permission rules apply. This matches the call style used by every other open() write in this package (e.g., frontend_runner.py for the bootstrap output file).

The other secure_open call sites (the connection file in _background/backend_runner.py and the log buffer in _background/log_buffers.py) are intentionally unchanged.

What is the impact of this change?

On Windows, newly created adaptor config files inherit ACLs from their parent directory rather than receiving an owner-only DACL. On POSIX, behavior is unchanged in practice — secure_open was already only setting S_IWUSR | S_IRUSR via os.open, which open() of a fresh file effectively produces under typical default umasks for the directories these files land in.

No public API changes. The secure_open helper itself is untouched and remains available for callers (including the connection-file and log-buffer call sites) that want owner-only DACLs.

How was this change tested?

  • hatch run test — 439 passed, 36 skipped (Posix-only tests skipped on Windows host), 91.94% coverage.

  • hatch run lintruff check, black --check, and mypy all clean.

  • Updated test/openjd/adaptor_runtime/unit/adaptors/configuration/test_configuration_manager.py::TestEnsureConfigFile::test_create to patch builtins.open instead of secure_open and to assert the new keyword (mode="w" rather than open_mode="w").

  • Have you run the unit tests? Yes (see above).

Was this change documented?

  • Are relevant docstrings in the code base updated? No docstrings reference the removed secure_open call; none required updates.

Is this a breaking change?

No. _ensure_config_file is a module-private helper (leading underscore) and ConfigurationManager's public API is unchanged. Callers outside the package do not depend on the file-permission side effect of the create path; the public read/write APIs (get_default_config, get_user_config, build_config, etc.) behave identically.

Does this change impact security?

The change relaxes a previously-enforced owner-only DACL on Windows for a single file-create call site. The argument for doing so is that:

  1. The file in question is a config file whose containing directories already have appropriate access control.
  2. The previous behavior was inconsistent — the override only applied when the runtime created the file; if an admin or packager pre-placed <AdaptorName>.json in the package directory or elsewhere, no DACL override was ever applied to it. So no part of the system was relying on this file's ACL as a security boundary.
  3. The owner-only DACL was actively harmful in cross-account access scenarios, where the SID baked into the ACE no longer matches the accessing account.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

The configuration manager creates an empty <AdaptorName>.json on first
access if one doesn't exist. This previously went through secure_open(),
which on Windows replaces the file's DACL with a non-inheriting ACE that
grants only the current owner SID FILE_GENERIC_READ | FILE_GENERIC_WRITE
| DELETE. That overrides the ACLs a user would otherwise expect to be
inherited from the parent directory and can produce surprising access
errors when the same config file is accessed by a different account
than the one that created it.

There's no clear reason adaptor config files specifically need
owner-only DACLs; the parent directory's ACLs already control access in
the locations these files are written to. Switch the create path to
plain open() and let normal filesystem permission rules apply.

Signed-off-by: Brian Axelson <86568017+baxeaz@users.noreply.github.com>
@baxeaz baxeaz requested a review from a team as a code owner May 22, 2026 21:13
@baxeaz baxeaz merged commit 2d42366 into OpenJobDescription:mainline May 25, 2026
24 checks passed
Cherie-Chen added a commit to Cherie-Chen/deadline-cloud-for-unreal-engine-cheriech that referenced this pull request May 26, 2026
….9.0

Bump minimum required versions of OpenJobDescription dependencies to
the latest releases available on PyPI:

  openjd-adaptor-runtime: 0.8.1 -> 0.9.4
  openjd-model:           0.8.1 -> 0.9.0

The adaptor-runtime bump picks up the Windows DACL fix from
OpenJobDescription/openjd-adaptor-runtime-for-python#260, which prevents
the per-adaptor JSON config file from being permanently locked to the
SID of whichever account first created it. This avoids PermissionError
on multi-user Unreal worker hosts.

openjd-model 0.9.1 has a GitHub release tag but was never published to
PyPI, so 0.9.0 is the highest installable floor.

The existing '< 0.10' upper bound is preserved.
Cherie-Chen added a commit to Cherie-Chen/deadline-cloud-for-unreal-engine-cheriech that referenced this pull request May 26, 2026
….9.0

Bump minimum required versions of OpenJobDescription dependencies to
the latest releases available on PyPI:

  openjd-adaptor-runtime: 0.8.1 -> 0.9.4
  openjd-model:           0.8.1 -> 0.9.0

The adaptor-runtime bump picks up the Windows DACL fix from
OpenJobDescription/openjd-adaptor-runtime-for-python#260, which prevents
the per-adaptor JSON config file from being permanently locked to the
SID of whichever account first created it. This avoids PermissionError
on multi-user Unreal worker hosts.

openjd-model 0.9.1 has a GitHub release tag but was never published to
PyPI, so 0.9.0 is the highest installable floor.

The existing '< 0.10' upper bound is preserved.
Cherie-Chen added a commit to Cherie-Chen/deadline-cloud-for-unreal-engine-cheriech that referenced this pull request May 26, 2026
….9.0

Bump minimum required versions of OpenJobDescription dependencies to
the latest releases available on PyPI:

  openjd-adaptor-runtime: 0.8.1 -> 0.9.4
  openjd-model:           0.8.1 -> 0.9.0

The adaptor-runtime bump picks up the Windows DACL fix from
OpenJobDescription/openjd-adaptor-runtime-for-python#260, which prevents
the per-adaptor JSON config file from being permanently locked to the
SID of whichever account first created it. This avoids PermissionError
on multi-user Unreal worker hosts.

openjd-model 0.9.1 has a GitHub release tag but was never published to
PyPI, so 0.9.0 is the highest installable floor.

The existing '< 0.10' upper bound is preserved.

Signed-off-by: Cherie-Chen <58997764+Cherie-Chen@users.noreply.github.com>
Cherie-Chen added a commit to aws-deadline/deadline-cloud-for-unreal-engine that referenced this pull request May 26, 2026
….9.0 (#302)

Bump minimum required versions of OpenJobDescription dependencies to
the latest releases available on PyPI:

  openjd-adaptor-runtime: 0.8.1 -> 0.9.4
  openjd-model:           0.8.1 -> 0.9.0

The adaptor-runtime bump picks up the Windows DACL fix from
OpenJobDescription/openjd-adaptor-runtime-for-python#260, which prevents
the per-adaptor JSON config file from being permanently locked to the
SID of whichever account first created it. This avoids PermissionError
on multi-user Unreal worker hosts.

openjd-model 0.9.1 has a GitHub release tag but was never published to
PyPI, so 0.9.0 is the highest installable floor.

The existing '< 0.10' upper bound is preserved.

Signed-off-by: Cherie-Chen <58997764+Cherie-Chen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants